deltachat/
transport.rs

1//! # Message transport.
2//!
3//! A transport represents a single IMAP+SMTP configuration
4//! that is known to work at least once in the past.
5//!
6//! Transports are stored in the `transports` SQL table.
7//! Each transport is uniquely identified by its email address.
8//! The table stores both the login parameters entered by the user
9//! and configured list of connection candidates.
10
11use std::fmt;
12use std::pin::Pin;
13
14use anyhow::{Context as _, Result, bail, format_err};
15use deltachat_contact_tools::{EmailAddress, addr_normalize};
16use serde::{Deserialize, Serialize};
17
18use crate::config::Config;
19use crate::configure::server_params::{ServerParams, expand_param_vector};
20use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
21use crate::context::Context;
22use crate::ensure_and_debug_assert;
23use crate::events::EventType;
24use crate::login_param::EnteredLoginParam;
25use crate::net::load_connection_timestamp;
26use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
27use crate::sql::Sql;
28use crate::sync::{RemovedTransportData, SyncData, TransportData};
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub(crate) enum ConnectionSecurity {
32    /// Implicit TLS.
33    Tls,
34
35    // STARTTLS.
36    Starttls,
37
38    /// Plaintext.
39    Plain,
40}
41
42impl fmt::Display for ConnectionSecurity {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Tls => write!(f, "tls")?,
46            Self::Starttls => write!(f, "starttls")?,
47            Self::Plain => write!(f, "plain")?,
48        }
49        Ok(())
50    }
51}
52
53impl TryFrom<Socket> for ConnectionSecurity {
54    type Error = anyhow::Error;
55
56    fn try_from(socket: Socket) -> Result<Self> {
57        match socket {
58            Socket::Automatic => Err(format_err!("Socket security is not configured")),
59            Socket::Ssl => Ok(Self::Tls),
60            Socket::Starttls => Ok(Self::Starttls),
61            Socket::Plain => Ok(Self::Plain),
62        }
63    }
64}
65
66/// Values saved into `imap_certificate_checks`.
67#[derive(
68    Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
69)]
70#[repr(u32)]
71#[strum(serialize_all = "snake_case")]
72pub(crate) enum ConfiguredCertificateChecks {
73    /// Use configuration from the provider database.
74    /// If there is no provider database setting for certificate checks,
75    /// accept invalid certificates.
76    ///
77    /// Must not be saved by new versions.
78    ///
79    /// Previous Delta Chat versions before core 1.133.0
80    /// stored this in `configured_imap_certificate_checks`
81    /// if Automatic configuration
82    /// was selected, configuration with strict TLS checks failed
83    /// and configuration without strict TLS checks succeeded.
84    OldAutomatic = 0,
85
86    /// Ensure that TLS certificate is valid for the server hostname.
87    Strict = 1,
88
89    /// Accept certificates that are expired, self-signed
90    /// or otherwise not valid for the server hostname.
91    AcceptInvalidCertificates = 2,
92
93    /// Accept certificates that are expired, self-signed
94    /// or otherwise not valid for the server hostname.
95    ///
96    /// Alias to `AcceptInvalidCertificates` for compatibility.
97    AcceptInvalidCertificates2 = 3,
98
99    /// Use configuration from the provider database.
100    /// If there is no provider database setting for certificate checks,
101    /// apply strict checks to TLS certificates.
102    Automatic = 4,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
106pub(crate) struct ConnectionCandidate {
107    /// Server hostname or IP address.
108    pub host: String,
109
110    /// Server port.
111    pub port: u16,
112
113    /// Transport layer security.
114    pub security: ConnectionSecurity,
115}
116
117impl fmt::Display for ConnectionCandidate {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{}:{}:{}", &self.host, self.port, self.security)?;
120        Ok(())
121    }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub(crate) struct ConfiguredServerLoginParam {
126    pub connection: ConnectionCandidate,
127
128    /// Username.
129    pub user: String,
130}
131
132impl fmt::Display for ConfiguredServerLoginParam {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}:{}", self.connection, &self.user)?;
135        Ok(())
136    }
137}
138
139pub(crate) async fn prioritize_server_login_params(
140    sql: &Sql,
141    params: &[ConfiguredServerLoginParam],
142    alpn: &str,
143) -> Result<Vec<ConfiguredServerLoginParam>> {
144    let mut res: Vec<(Option<i64>, ConfiguredServerLoginParam)> = Vec::with_capacity(params.len());
145    for param in params {
146        let timestamp = load_connection_timestamp(
147            sql,
148            alpn,
149            &param.connection.host,
150            param.connection.port,
151            None,
152        )
153        .await?;
154        res.push((timestamp, param.clone()));
155    }
156    res.sort_by_key(|(ts, _param)| std::cmp::Reverse(*ts));
157    Ok(res.into_iter().map(|(_ts, param)| param).collect())
158}
159
160/// Login parameters saved to the database
161/// after successful configuration.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub(crate) struct ConfiguredLoginParam {
164    /// `From:` address that was used at the time of configuration.
165    pub addr: String,
166
167    /// List of IMAP candidates to try.
168    pub imap: Vec<ConfiguredServerLoginParam>,
169
170    /// Custom IMAP user.
171    ///
172    /// This overwrites autoconfig from the provider database
173    /// if non-empty.
174    pub imap_user: String,
175
176    pub imap_password: String,
177
178    // IMAP folder to watch.
179    //
180    // If not stored, should be interpreted as "INBOX".
181    // If stored, should be a folder name and not empty.
182    pub imap_folder: Option<String>,
183
184    /// List of SMTP candidates to try.
185    pub smtp: Vec<ConfiguredServerLoginParam>,
186
187    /// Custom SMTP user.
188    ///
189    /// This overwrites autoconfig from the provider database
190    /// if non-empty.
191    pub smtp_user: String,
192
193    pub smtp_password: String,
194
195    pub provider: Option<&'static Provider>,
196
197    /// TLS options: whether to allow invalid certificates and/or
198    /// invalid hostnames
199    pub certificate_checks: ConfiguredCertificateChecks,
200
201    /// If true, login via OAUTH2 (not recommended anymore)
202    pub oauth2: bool,
203}
204
205/// JSON representation of ConfiguredLoginParam
206/// for the database and sync messages.
207#[derive(Debug, Serialize, Deserialize)]
208pub(crate) struct ConfiguredLoginParamJson {
209    pub addr: String,
210    pub imap: Vec<ConfiguredServerLoginParam>,
211
212    /// IMAP folder to watch.
213    ///
214    /// Defaults to "INBOX" if unset.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub imap_folder: Option<String>,
217
218    pub imap_user: String,
219    pub imap_password: String,
220    pub smtp: Vec<ConfiguredServerLoginParam>,
221    pub smtp_user: String,
222    pub smtp_password: String,
223    pub provider_id: Option<String>,
224    pub certificate_checks: ConfiguredCertificateChecks,
225    pub oauth2: bool,
226}
227
228impl fmt::Display for ConfiguredLoginParam {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        let addr = &self.addr;
231        let provider_id = match self.provider {
232            Some(provider) => provider.id,
233            None => "none",
234        };
235        let certificate_checks = self.certificate_checks;
236        write!(f, "{addr} imap:[")?;
237        let mut first = true;
238        for imap in &self.imap {
239            if !first {
240                write!(f, ", ")?;
241            }
242            write!(f, "{imap}")?;
243            first = false;
244        }
245        write!(f, "] smtp:[")?;
246        let mut first = true;
247        for smtp in &self.smtp {
248            if !first {
249                write!(f, ", ")?;
250            }
251            write!(f, "{smtp}")?;
252            first = false;
253        }
254        write!(f, "] provider:{provider_id} cert_{certificate_checks}")?;
255        Ok(())
256    }
257}
258
259impl ConfiguredLoginParam {
260    /// Load configured account settings from the database.
261    ///
262    /// Returns transport ID and configured parameters
263    /// of the current primary transport.
264    /// Returns `None` if account is not configured.
265    pub(crate) async fn load(context: &Context) -> Result<Option<(u32, Self)>> {
266        let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
267            return Ok(None);
268        };
269
270        let Some((id, json)) = context
271            .sql
272            .query_row_optional(
273                "SELECT id, configured_param FROM transports WHERE addr=?",
274                (&self_addr,),
275                |row| {
276                    let id: u32 = row.get(0)?;
277                    let json: String = row.get(1)?;
278                    Ok((id, json))
279                },
280            )
281            .await?
282        else {
283            bail!("Self address {self_addr} doesn't have a corresponding transport");
284        };
285        Ok(Some((id, Self::from_json(&json)?)))
286    }
287
288    /// Loads configured login parameters for all transports.
289    ///
290    /// Returns a vector of all transport IDs
291    /// paired with the configured parameters for the transports.
292    pub(crate) async fn load_all(context: &Context) -> Result<Vec<(u32, Self)>> {
293        context
294            .sql
295            .query_map_vec("SELECT id, configured_param FROM transports", (), |row| {
296                let id: u32 = row.get(0)?;
297                let json: String = row.get(1)?;
298                let param = Self::from_json(&json)?;
299                Ok((id, param))
300            })
301            .await
302    }
303
304    /// Loads legacy configured param. Only used for tests and the migration.
305    pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
306        if !context.get_config_bool(Config::Configured).await? {
307            return Ok(None);
308        }
309
310        let addr = context
311            .get_config(Config::ConfiguredAddr)
312            .await?
313            .unwrap_or_default()
314            .trim()
315            .to_string();
316
317        let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) =
318            context
319                .get_config_parsed::<i32>(Config::ConfiguredImapCertificateChecks)
320                .await?
321        {
322            num_traits::FromPrimitive::from_i32(certificate_checks)
323                .context("Invalid configured_imap_certificate_checks value")?
324        } else {
325            // This is true for old accounts configured using C core
326            // which did not check TLS certificates.
327            ConfiguredCertificateChecks::OldAutomatic
328        };
329
330        let send_pw = context
331            .get_config(Config::ConfiguredSendPw)
332            .await?
333            .context("SMTP password is not configured")?;
334        let mail_pw = context
335            .get_config(Config::ConfiguredMailPw)
336            .await?
337            .context("IMAP password is not configured")?;
338
339        let server_flags = context
340            .get_config_parsed::<i32>(Config::ConfiguredServerFlags)
341            .await?
342            .unwrap_or_default();
343        let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
344
345        let provider = context
346            .get_config(Config::ConfiguredProvider)
347            .await?
348            .and_then(|cfg| get_provider_by_id(&cfg));
349
350        let imap;
351        let smtp;
352
353        let mail_user = context
354            .get_config(Config::ConfiguredMailUser)
355            .await?
356            .unwrap_or_default();
357        let send_user = context
358            .get_config(Config::ConfiguredSendUser)
359            .await?
360            .unwrap_or_default();
361
362        if let Some(provider) = provider {
363            let parsed_addr = EmailAddress::new(&addr).context("Bad email-address")?;
364            let addr_localpart = parsed_addr.local;
365
366            if provider.server.is_empty() {
367                let servers = vec![
368                    ServerParams {
369                        protocol: Protocol::Imap,
370                        hostname: context
371                            .get_config(Config::ConfiguredMailServer)
372                            .await?
373                            .unwrap_or_default(),
374                        port: context
375                            .get_config_parsed::<u16>(Config::ConfiguredMailPort)
376                            .await?
377                            .unwrap_or_default(),
378                        socket: context
379                            .get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
380                            .await?
381                            .and_then(num_traits::FromPrimitive::from_i32)
382                            .unwrap_or_default(),
383                        username: mail_user.clone(),
384                    },
385                    ServerParams {
386                        protocol: Protocol::Smtp,
387                        hostname: context
388                            .get_config(Config::ConfiguredSendServer)
389                            .await?
390                            .unwrap_or_default(),
391                        port: context
392                            .get_config_parsed::<u16>(Config::ConfiguredSendPort)
393                            .await?
394                            .unwrap_or_default(),
395                        socket: context
396                            .get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
397                            .await?
398                            .and_then(num_traits::FromPrimitive::from_i32)
399                            .unwrap_or_default(),
400                        username: send_user.clone(),
401                    },
402                ];
403                let servers = expand_param_vector(servers, &addr, &parsed_addr.domain);
404                imap = servers
405                    .iter()
406                    .filter_map(|params| {
407                        let Ok(security) = params.socket.try_into() else {
408                            return None;
409                        };
410                        if params.protocol == Protocol::Imap {
411                            Some(ConfiguredServerLoginParam {
412                                connection: ConnectionCandidate {
413                                    host: params.hostname.clone(),
414                                    port: params.port,
415                                    security,
416                                },
417                                user: params.username.clone(),
418                            })
419                        } else {
420                            None
421                        }
422                    })
423                    .collect();
424                smtp = servers
425                    .iter()
426                    .filter_map(|params| {
427                        let Ok(security) = params.socket.try_into() else {
428                            return None;
429                        };
430                        if params.protocol == Protocol::Smtp {
431                            Some(ConfiguredServerLoginParam {
432                                connection: ConnectionCandidate {
433                                    host: params.hostname.clone(),
434                                    port: params.port,
435                                    security,
436                                },
437                                user: params.username.clone(),
438                            })
439                        } else {
440                            None
441                        }
442                    })
443                    .collect();
444            } else {
445                imap = provider
446                    .server
447                    .iter()
448                    .filter_map(|server| {
449                        if server.protocol != Protocol::Imap {
450                            return None;
451                        }
452
453                        let Ok(security) = server.socket.try_into() else {
454                            return None;
455                        };
456
457                        Some(ConfiguredServerLoginParam {
458                            connection: ConnectionCandidate {
459                                host: server.hostname.to_string(),
460                                port: server.port,
461                                security,
462                            },
463                            user: if !mail_user.is_empty() {
464                                mail_user.clone()
465                            } else {
466                                match server.username_pattern {
467                                    UsernamePattern::Email => addr.to_string(),
468                                    UsernamePattern::Emaillocalpart => addr_localpart.clone(),
469                                }
470                            },
471                        })
472                    })
473                    .collect();
474                smtp = provider
475                    .server
476                    .iter()
477                    .filter_map(|server| {
478                        if server.protocol != Protocol::Smtp {
479                            return None;
480                        }
481
482                        let Ok(security) = server.socket.try_into() else {
483                            return None;
484                        };
485
486                        Some(ConfiguredServerLoginParam {
487                            connection: ConnectionCandidate {
488                                host: server.hostname.to_string(),
489                                port: server.port,
490                                security,
491                            },
492                            user: if !send_user.is_empty() {
493                                send_user.clone()
494                            } else {
495                                match server.username_pattern {
496                                    UsernamePattern::Email => addr.to_string(),
497                                    UsernamePattern::Emaillocalpart => addr_localpart.clone(),
498                                }
499                            },
500                        })
501                    })
502                    .collect();
503            }
504        } else if let (Some(configured_mail_servers), Some(configured_send_servers)) = (
505            context.get_config(Config::ConfiguredImapServers).await?,
506            context.get_config(Config::ConfiguredSmtpServers).await?,
507        ) {
508            imap = serde_json::from_str(&configured_mail_servers)
509                .context("Failed to parse configured IMAP servers")?;
510            smtp = serde_json::from_str(&configured_send_servers)
511                .context("Failed to parse configured SMTP servers")?;
512        } else {
513            // Load legacy settings storing a single IMAP and single SMTP server.
514            let mail_server = context
515                .get_config(Config::ConfiguredMailServer)
516                .await?
517                .unwrap_or_default();
518            let mail_port = context
519                .get_config_parsed::<u16>(Config::ConfiguredMailPort)
520                .await?
521                .unwrap_or_default();
522
523            let mail_security: Socket = context
524                .get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
525                .await?
526                .and_then(num_traits::FromPrimitive::from_i32)
527                .unwrap_or_default();
528
529            let send_server = context
530                .get_config(Config::ConfiguredSendServer)
531                .await?
532                .context("SMTP server is not configured")?;
533            let send_port = context
534                .get_config_parsed::<u16>(Config::ConfiguredSendPort)
535                .await?
536                .unwrap_or_default();
537            let send_security: Socket = context
538                .get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
539                .await?
540                .and_then(num_traits::FromPrimitive::from_i32)
541                .unwrap_or_default();
542
543            imap = vec![ConfiguredServerLoginParam {
544                connection: ConnectionCandidate {
545                    host: mail_server,
546                    port: mail_port,
547                    security: mail_security.try_into()?,
548                },
549                user: mail_user.clone(),
550            }];
551            smtp = vec![ConfiguredServerLoginParam {
552                connection: ConnectionCandidate {
553                    host: send_server,
554                    port: send_port,
555                    security: send_security.try_into()?,
556                },
557                user: send_user.clone(),
558            }];
559        }
560
561        Ok(Some(ConfiguredLoginParam {
562            addr,
563            imap,
564            imap_folder: None,
565            imap_user: mail_user,
566            imap_password: mail_pw,
567            smtp,
568            smtp_user: send_user,
569            smtp_password: send_pw,
570            certificate_checks,
571            provider,
572            oauth2,
573        }))
574    }
575
576    pub(crate) async fn save_to_transports_table(
577        self,
578        context: &Context,
579        entered_param: &EnteredLoginParam,
580        timestamp: i64,
581    ) -> Result<()> {
582        let is_published = true;
583        save_transport(
584            context,
585            entered_param,
586            &self.into(),
587            timestamp,
588            is_published,
589        )
590        .await?;
591        Ok(())
592    }
593
594    pub(crate) fn from_json(json: &str) -> Result<Self> {
595        let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
596
597        ensure_and_debug_assert!(
598            json.imap_folder
599                .as_ref()
600                .is_none_or(|folder| !folder.is_empty()),
601            "Configured watched folder name cannot be empty"
602        );
603        let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
604
605        Ok(ConfiguredLoginParam {
606            addr: json.addr,
607            imap: json.imap,
608            imap_folder: json.imap_folder,
609            imap_user: json.imap_user,
610            imap_password: json.imap_password,
611            smtp: json.smtp,
612            smtp_user: json.smtp_user,
613            smtp_password: json.smtp_password,
614            provider,
615            certificate_checks: json.certificate_checks,
616            oauth2: json.oauth2,
617        })
618    }
619
620    pub(crate) fn into_json(self) -> Result<String> {
621        let json: ConfiguredLoginParamJson = self.into();
622        Ok(serde_json::to_string(&json)?)
623    }
624
625    pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
626        let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
627        match self.certificate_checks {
628            ConfiguredCertificateChecks::OldAutomatic => {
629                provider_strict_tls.unwrap_or(connected_through_proxy)
630            }
631            ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
632            ConfiguredCertificateChecks::Strict => true,
633            ConfiguredCertificateChecks::AcceptInvalidCertificates
634            | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
635        }
636    }
637}
638
639impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
640    fn from(configured_login_param: ConfiguredLoginParam) -> Self {
641        Self {
642            addr: configured_login_param.addr,
643            imap: configured_login_param.imap,
644            imap_user: configured_login_param.imap_user,
645            imap_password: configured_login_param.imap_password,
646            imap_folder: configured_login_param.imap_folder,
647            smtp: configured_login_param.smtp,
648            smtp_user: configured_login_param.smtp_user,
649            smtp_password: configured_login_param.smtp_password,
650            provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
651            certificate_checks: configured_login_param.certificate_checks,
652            oauth2: configured_login_param.oauth2,
653        }
654    }
655}
656
657/// Saves transport to the database.
658/// Returns whether transports are modified.
659pub(crate) async fn save_transport(
660    context: &Context,
661    entered_param: &EnteredLoginParam,
662    configured: &ConfiguredLoginParamJson,
663    add_timestamp: i64,
664    is_published: bool,
665) -> Result<bool> {
666    ensure_and_debug_assert!(
667        configured
668            .imap_folder
669            .as_ref()
670            .is_none_or(|folder| !folder.is_empty()),
671        "Configured watched folder name cannot be empty"
672    );
673
674    let addr = addr_normalize(&configured.addr);
675    let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
676    let mut modified = context
677        .sql
678        .execute(
679            "INSERT INTO transports (addr, entered_param, configured_param, add_timestamp, is_published)
680             VALUES (?, ?, ?, ?, ?)
681             ON CONFLICT (addr)
682             DO UPDATE SET entered_param=excluded.entered_param,
683                           configured_param=excluded.configured_param,
684                           add_timestamp=excluded.add_timestamp,
685                           is_published=excluded.is_published
686             WHERE entered_param != excluded.entered_param
687                 OR configured_param != excluded.configured_param
688                 OR add_timestamp < excluded.add_timestamp
689                 OR is_published != excluded.is_published",
690            (
691                &addr,
692                serde_json::to_string(entered_param)?,
693                serde_json::to_string(configured)?,
694                add_timestamp,
695                is_published,
696            ),
697        )
698        .await?
699        > 0;
700
701    if configured_addr.is_none() {
702        // If there is no transport yet, set the new transport as the primary one
703        context
704            .sql
705            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
706            .await?;
707        modified = true;
708    }
709    Ok(modified)
710}
711
712/// Sends a sync message to synchronize transports across devices.
713pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
714    info!(context, "Sending transport synchronization message.");
715
716    // Regenerate public key to include all transports.
717    context.self_public_key.lock().await.take();
718
719    // Synchronize all transport configurations.
720    //
721    // Transport with ID 1 is never synchronized
722    // because it can only be created during initial configuration.
723    // This also guarantees that credentials for the first
724    // transport are never sent in sync messages,
725    // so this is not worse than when not using multi-transport.
726    // If transport ID 1 is reconfigured,
727    // likely because the password has changed,
728    // user has to reconfigure it manually on all devices.
729    let transports = context
730        .sql
731        .query_map_vec(
732            "SELECT entered_param, configured_param, add_timestamp, is_published
733             FROM transports WHERE id>1",
734            (),
735            |row| {
736                let entered_json: String = row.get(0)?;
737                let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
738                let configured_json: String = row.get(1)?;
739                let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
740                let timestamp: i64 = row.get(2)?;
741                let is_published: bool = row.get(3)?;
742                Ok(TransportData {
743                    configured,
744                    entered,
745                    timestamp,
746                    is_published,
747                })
748            },
749        )
750        .await?;
751    let removed_transports = context
752        .sql
753        .query_map_vec(
754            "SELECT addr, remove_timestamp FROM removed_transports",
755            (),
756            |row| {
757                let addr: String = row.get(0)?;
758                let timestamp: i64 = row.get(1)?;
759                Ok(RemovedTransportData { addr, timestamp })
760            },
761        )
762        .await?;
763    context
764        .add_sync_item(SyncData::Transports {
765            transports,
766            removed_transports,
767        })
768        .await?;
769    context.scheduler.interrupt_smtp().await;
770
771    Ok(())
772}
773
774/// Process received data for transport synchronization.
775pub(crate) async fn sync_transports(
776    context: &Context,
777    transports: &[TransportData],
778    removed_transports: &[RemovedTransportData],
779) -> Result<()> {
780    let mut modified = false;
781    for TransportData {
782        configured,
783        entered,
784        timestamp,
785        is_published,
786    } in transports
787    {
788        modified |= save_transport(context, entered, configured, *timestamp, *is_published).await?;
789    }
790
791    context
792        .sql
793        .transaction(|transaction| {
794            for RemovedTransportData { addr, timestamp } in removed_transports {
795                modified |= transaction.execute(
796                    "DELETE FROM transports
797                     WHERE addr=? AND add_timestamp<=?",
798                    (addr, timestamp),
799                )? > 0;
800                transaction.execute(
801                    "INSERT INTO removed_transports (addr, remove_timestamp)
802                     VALUES (?, ?)
803                     ON CONFLICT (addr) DO
804                     UPDATE SET remove_timestamp = excluded.remove_timestamp
805                     WHERE excluded.remove_timestamp > remove_timestamp",
806                    (addr, timestamp),
807                )?;
808            }
809            Ok(())
810        })
811        .await?;
812
813    if modified {
814        context.self_public_key.lock().await.take();
815        tokio::task::spawn(restart_io_if_running_boxed(context.clone()));
816        context.emit_event(EventType::TransportsModified);
817    }
818    Ok(())
819}
820
821/// Same as `context.restart_io_if_running()`, but `Box::pin`ed and with a `+ Send` bound,
822/// so that it can be called recursively.
823fn restart_io_if_running_boxed(context: Context) -> Pin<Box<dyn Future<Output = ()> + Send>> {
824    Box::pin(async move { context.restart_io_if_running().await })
825}
826
827/// Adds transport entry to the `transports` table with empty configuration.
828pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
829    context.sql
830        .execute(
831            "INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
832            (
833                addr,
834                serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
835                format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
836            ),
837        )
838        .await?;
839    Ok(())
840}
841
842#[cfg(test)]
843mod transport_tests;