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