Skip to main content

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