tuwunel_service/migrations/
email_bindings.rs1use futures::StreamExt;
2use ruma::{MilliSecondsSinceUnixEpoch, ServerName, thirdparty::Medium};
3use tuwunel_core::{Result, debug_warn, err, info, warn};
4
5use super::local_user_id;
6use crate::{Services, threepid::canonicalize_email};
7
8pub(super) async fn migrate_email_bindings(services: &Services) -> Result {
20 let Some(localpart_email) = services.db.open_cf("localpart_email")? else {
21 return Ok(());
22 };
23
24 let server_name = services.globals.server_name();
25 let bound_at = MilliSecondsSinceUnixEpoch::now();
26 let cork = services.db.cork_and_sync();
27
28 let (adopted, skipped, unreadable) = localpart_email
29 .stream()
30 .fold((0_usize, 0_usize, 0_usize), async |acc, binding: Result<(&str, &str)>| {
31 let adopted = match binding {
32 | Err(e) => Err(e),
33 | Ok((localpart, address)) =>
34 adopt_one(services, server_name, bound_at, localpart, address).await,
35 };
36
37 tally_adoption(acc, adopted)
38 })
39 .await;
40
41 drop(cork);
42
43 match skipped {
44 | 0 if adopted > 0 => info!(%adopted, "Adopted email bindings from a foreign database"),
45 | 0 => (),
46 | _ => warn!(
47 %adopted,
48 %skipped,
49 "Adopted email bindings from a foreign database; some addresses were left behind"
50 ),
51 }
52
53 unreadable
56 .eq(&0)
57 .then_some(())
58 .ok_or_else(|| err!(Database("{unreadable} email bindings could not be read")))
59}
60
61async fn adopt_one(
68 services: &Services,
69 server_name: &ServerName,
70 bound_at: MilliSecondsSinceUnixEpoch,
71 localpart: &str,
72 address: &str,
73) -> Result<bool> {
74 let Some(user_id) = local_user_id(localpart, server_name) else {
75 debug_warn!(%localpart, "skipping an unusable localpart");
76 return Ok(false);
77 };
78
79 match services.db["userid_password"].get(&user_id).await {
82 | Ok(_) if user_id != services.globals.server_user => (),
83 | Ok(_) => return Ok(false),
84 | Err(e) if e.is_not_found() => return Ok(false),
85 | Err(e) => return Err(e),
86 }
87
88 let Ok(email_canon) = canonicalize_email(address) else {
89 debug_warn!(%localpart, "skipping an unusable address");
90 return Ok(false);
91 };
92
93 if services
94 .threepid
95 .bound_elsewhere(&user_id, &email_canon)
96 .await?
97 {
98 return Ok(false);
99 }
100
101 services
102 .threepid
103 .put_binding(&user_id, &email_canon, Medium::Email, bound_at, bound_at)
104 .await;
105
106 Ok(true)
107}
108
109fn tally_adoption(
110 (adopted, skipped, unreadable): (usize, usize, usize),
111 result: Result<bool>,
112) -> (usize, usize, usize) {
113 match result {
114 | Ok(true) => (adopted.saturating_add(1), skipped, unreadable),
115 | Ok(false) => (adopted, skipped.saturating_add(1), unreadable),
116 | Err(e) => {
117 warn!(error = %e, "an email binding could not be read");
118
119 (adopted, skipped, unreadable.saturating_add(1))
120 },
121 }
122}