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;
12
13use anyhow::{Context as _, Result, bail, format_err};
14use deltachat_contact_tools::{EmailAddress, addr_normalize};
15use serde::{Deserialize, Serialize};
16
17use crate::config::Config;
18use crate::configure::server_params::{ServerParams, expand_param_vector};
19use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
20use crate::context::Context;
21use crate::events::EventType;
22use crate::login_param::EnteredLoginParam;
23use crate::net::load_connection_timestamp;
24use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
25use crate::sql::Sql;
26use crate::sync::{RemovedTransportData, SyncData, TransportData};
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub(crate) enum ConnectionSecurity {
30    /// Implicit TLS.
31    Tls,
32
33    // STARTTLS.
34    Starttls,
35
36    /// Plaintext.
37    Plain,
38}
39
40impl fmt::Display for ConnectionSecurity {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Tls => write!(f, "tls")?,
44            Self::Starttls => write!(f, "starttls")?,
45            Self::Plain => write!(f, "plain")?,
46        }
47        Ok(())
48    }
49}
50
51impl TryFrom<Socket> for ConnectionSecurity {
52    type Error = anyhow::Error;
53
54    fn try_from(socket: Socket) -> Result<Self> {
55        match socket {
56            Socket::Automatic => Err(format_err!("Socket security is not configured")),
57            Socket::Ssl => Ok(Self::Tls),
58            Socket::Starttls => Ok(Self::Starttls),
59            Socket::Plain => Ok(Self::Plain),
60        }
61    }
62}
63
64/// Values saved into `imap_certificate_checks`.
65#[derive(
66    Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
67)]
68#[repr(u32)]
69#[strum(serialize_all = "snake_case")]
70pub(crate) enum ConfiguredCertificateChecks {
71    /// Use configuration from the provider database.
72    /// If there is no provider database setting for certificate checks,
73    /// accept invalid certificates.
74    ///
75    /// Must not be saved by new versions.
76    ///
77    /// Previous Delta Chat versions before core 1.133.0
78    /// stored this in `configured_imap_certificate_checks`
79    /// if Automatic configuration
80    /// was selected, configuration with strict TLS checks failed
81    /// and configuration without strict TLS checks succeeded.
82    OldAutomatic = 0,
83
84    /// Ensure that TLS certificate is valid for the server hostname.
85    Strict = 1,
86
87    /// Accept certificates that are expired, self-signed
88    /// or otherwise not valid for the server hostname.
89    AcceptInvalidCertificates = 2,
90
91    /// Accept certificates that are expired, self-signed
92    /// or otherwise not valid for the server hostname.
93    ///
94    /// Alias to `AcceptInvalidCertificates` for compatibility.
95    AcceptInvalidCertificates2 = 3,
96
97    /// Use configuration from the provider database.
98    /// If there is no provider database setting for certificate checks,
99    /// apply strict checks to TLS certificates.
100    Automatic = 4,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104pub(crate) struct ConnectionCandidate {
105    /// Server hostname or IP address.
106    pub host: String,
107
108    /// Server port.
109    pub port: u16,
110
111    /// Transport layer security.
112    pub security: ConnectionSecurity,
113}
114
115impl fmt::Display for ConnectionCandidate {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "{}:{}:{}", &self.host, self.port, self.security)?;
118        Ok(())
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub(crate) struct ConfiguredServerLoginParam {
124    pub connection: ConnectionCandidate,
125
126    /// Username.
127    pub user: String,
128}
129
130impl fmt::Display for ConfiguredServerLoginParam {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{}:{}", self.connection, &self.user)?;
133        Ok(())
134    }
135}
136
137pub(crate) async fn prioritize_server_login_params(
138    sql: &Sql,
139    params: &[ConfiguredServerLoginParam],
140    alpn: &str,
141) -> Result<Vec<ConfiguredServerLoginParam>> {
142    let mut res: Vec<(Option<i64>, ConfiguredServerLoginParam)> = Vec::with_capacity(params.len());
143    for param in params {
144        let timestamp = load_connection_timestamp(
145            sql,
146            alpn,
147            &param.connection.host,
148            param.connection.port,
149            None,
150        )
151        .await?;
152        res.push((timestamp, param.clone()));
153    }
154    res.sort_by_key(|(ts, _param)| std::cmp::Reverse(*ts));
155    Ok(res.into_iter().map(|(_ts, param)| param).collect())
156}
157
158/// Login parameters saved to the database
159/// after successful configuration.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub(crate) struct ConfiguredLoginParam {
162    /// `From:` address that was used at the time of configuration.
163    pub addr: String,
164
165    pub imap: Vec<ConfiguredServerLoginParam>,
166
167    // Custom IMAP user.
168    //
169    // This overwrites autoconfig from the provider database
170    // if non-empty.
171    pub imap_user: String,
172
173    pub imap_password: String,
174
175    pub smtp: Vec<ConfiguredServerLoginParam>,
176
177    // Custom SMTP user.
178    //
179    // This overwrites autoconfig from the provider database
180    // if non-empty.
181    pub smtp_user: String,
182
183    pub smtp_password: String,
184
185    pub provider: Option<&'static Provider>,
186
187    /// TLS options: whether to allow invalid certificates and/or
188    /// invalid hostnames
189    pub certificate_checks: ConfiguredCertificateChecks,
190
191    /// If true, login via OAUTH2 (not recommended anymore)
192    pub oauth2: bool,
193}
194
195/// JSON representation of ConfiguredLoginParam
196/// for the database and sync messages.
197#[derive(Debug, Serialize, Deserialize)]
198pub(crate) struct ConfiguredLoginParamJson {
199    pub addr: String,
200    pub imap: Vec<ConfiguredServerLoginParam>,
201    pub imap_user: String,
202    pub imap_password: String,
203    pub smtp: Vec<ConfiguredServerLoginParam>,
204    pub smtp_user: String,
205    pub smtp_password: String,
206    pub provider_id: Option<String>,
207    pub certificate_checks: ConfiguredCertificateChecks,
208    pub oauth2: bool,
209}
210
211impl fmt::Display for ConfiguredLoginParam {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        let addr = &self.addr;
214        let provider_id = match self.provider {
215            Some(provider) => provider.id,
216            None => "none",
217        };
218        let certificate_checks = self.certificate_checks;
219        write!(f, "{addr} imap:[")?;
220        let mut first = true;
221        for imap in &self.imap {
222            if !first {
223                write!(f, ", ")?;
224            }
225            write!(f, "{imap}")?;
226            first = false;
227        }
228        write!(f, "] smtp:[")?;
229        let mut first = true;
230        for smtp in &self.smtp {
231            if !first {
232                write!(f, ", ")?;
233            }
234            write!(f, "{smtp}")?;
235            first = false;
236        }
237        write!(f, "] provider:{provider_id} cert_{certificate_checks}")?;
238        Ok(())
239    }
240}
241
242impl ConfiguredLoginParam {
243    /// Load configured account settings from the database.
244    ///
245    /// Returns transport ID and configured parameters
246    /// of the current primary transport.
247    /// Returns `None` if account is not configured.
248    pub(crate) async fn load(context: &Context) -> Result<Option<(u32, Self)>> {
249        let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
250            return Ok(None);
251        };
252
253        let Some((id, json)) = context
254            .sql
255            .query_row_optional(
256                "SELECT id, configured_param FROM transports WHERE addr=?",
257                (&self_addr,),
258                |row| {
259                    let id: u32 = row.get(0)?;
260                    let json: String = row.get(1)?;
261                    Ok((id, json))
262                },
263            )
264            .await?
265        else {
266            bail!("Self address {self_addr} doesn't have a corresponding transport");
267        };
268        Ok(Some((id, Self::from_json(&json)?)))
269    }
270
271    /// Loads configured login parameters for all transports.
272    ///
273    /// Returns a vector of all transport IDs
274    /// paired with the configured parameters for the transports.
275    pub(crate) async fn load_all(context: &Context) -> Result<Vec<(u32, Self)>> {
276        context
277            .sql
278            .query_map_vec("SELECT id, configured_param FROM transports", (), |row| {
279                let id: u32 = row.get(0)?;
280                let json: String = row.get(1)?;
281                let param = Self::from_json(&json)?;
282                Ok((id, param))
283            })
284            .await
285    }
286
287    /// Loads legacy configured param. Only used for tests and the migration.
288    pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
289        if !context.get_config_bool(Config::Configured).await? {
290            return Ok(None);
291        }
292
293        let addr = context
294            .get_config(Config::ConfiguredAddr)
295            .await?
296            .unwrap_or_default()
297            .trim()
298            .to_string();
299
300        let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) =
301            context
302                .get_config_parsed::<i32>(Config::ConfiguredImapCertificateChecks)
303                .await?
304        {
305            num_traits::FromPrimitive::from_i32(certificate_checks)
306                .context("Invalid configured_imap_certificate_checks value")?
307        } else {
308            // This is true for old accounts configured using C core
309            // which did not check TLS certificates.
310            ConfiguredCertificateChecks::OldAutomatic
311        };
312
313        let send_pw = context
314            .get_config(Config::ConfiguredSendPw)
315            .await?
316            .context("SMTP password is not configured")?;
317        let mail_pw = context
318            .get_config(Config::ConfiguredMailPw)
319            .await?
320            .context("IMAP password is not configured")?;
321
322        let server_flags = context
323            .get_config_parsed::<i32>(Config::ConfiguredServerFlags)
324            .await?
325            .unwrap_or_default();
326        let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
327
328        let provider = context
329            .get_config(Config::ConfiguredProvider)
330            .await?
331            .and_then(|cfg| get_provider_by_id(&cfg));
332
333        let imap;
334        let smtp;
335
336        let mail_user = context
337            .get_config(Config::ConfiguredMailUser)
338            .await?
339            .unwrap_or_default();
340        let send_user = context
341            .get_config(Config::ConfiguredSendUser)
342            .await?
343            .unwrap_or_default();
344
345        if let Some(provider) = provider {
346            let parsed_addr = EmailAddress::new(&addr).context("Bad email-address")?;
347            let addr_localpart = parsed_addr.local;
348
349            if provider.server.is_empty() {
350                let servers = vec![
351                    ServerParams {
352                        protocol: Protocol::Imap,
353                        hostname: context
354                            .get_config(Config::ConfiguredMailServer)
355                            .await?
356                            .unwrap_or_default(),
357                        port: context
358                            .get_config_parsed::<u16>(Config::ConfiguredMailPort)
359                            .await?
360                            .unwrap_or_default(),
361                        socket: context
362                            .get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
363                            .await?
364                            .and_then(num_traits::FromPrimitive::from_i32)
365                            .unwrap_or_default(),
366                        username: mail_user.clone(),
367                    },
368                    ServerParams {
369                        protocol: Protocol::Smtp,
370                        hostname: context
371                            .get_config(Config::ConfiguredSendServer)
372                            .await?
373                            .unwrap_or_default(),
374                        port: context
375                            .get_config_parsed::<u16>(Config::ConfiguredSendPort)
376                            .await?
377                            .unwrap_or_default(),
378                        socket: context
379                            .get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
380                            .await?
381                            .and_then(num_traits::FromPrimitive::from_i32)
382                            .unwrap_or_default(),
383                        username: send_user.clone(),
384                    },
385                ];
386                let servers = expand_param_vector(servers, &addr, &parsed_addr.domain);
387                imap = servers
388                    .iter()
389                    .filter_map(|params| {
390                        let Ok(security) = params.socket.try_into() else {
391                            return None;
392                        };
393                        if params.protocol == Protocol::Imap {
394                            Some(ConfiguredServerLoginParam {
395                                connection: ConnectionCandidate {
396                                    host: params.hostname.clone(),
397                                    port: params.port,
398                                    security,
399                                },
400                                user: params.username.clone(),
401                            })
402                        } else {
403                            None
404                        }
405                    })
406                    .collect();
407                smtp = servers
408                    .iter()
409                    .filter_map(|params| {
410                        let Ok(security) = params.socket.try_into() else {
411                            return None;
412                        };
413                        if params.protocol == Protocol::Smtp {
414                            Some(ConfiguredServerLoginParam {
415                                connection: ConnectionCandidate {
416                                    host: params.hostname.clone(),
417                                    port: params.port,
418                                    security,
419                                },
420                                user: params.username.clone(),
421                            })
422                        } else {
423                            None
424                        }
425                    })
426                    .collect();
427            } else {
428                imap = provider
429                    .server
430                    .iter()
431                    .filter_map(|server| {
432                        if server.protocol != Protocol::Imap {
433                            return None;
434                        }
435
436                        let Ok(security) = server.socket.try_into() else {
437                            return None;
438                        };
439
440                        Some(ConfiguredServerLoginParam {
441                            connection: ConnectionCandidate {
442                                host: server.hostname.to_string(),
443                                port: server.port,
444                                security,
445                            },
446                            user: if !mail_user.is_empty() {
447                                mail_user.clone()
448                            } else {
449                                match server.username_pattern {
450                                    UsernamePattern::Email => addr.to_string(),
451                                    UsernamePattern::Emaillocalpart => addr_localpart.clone(),
452                                }
453                            },
454                        })
455                    })
456                    .collect();
457                smtp = provider
458                    .server
459                    .iter()
460                    .filter_map(|server| {
461                        if server.protocol != Protocol::Smtp {
462                            return None;
463                        }
464
465                        let Ok(security) = server.socket.try_into() else {
466                            return None;
467                        };
468
469                        Some(ConfiguredServerLoginParam {
470                            connection: ConnectionCandidate {
471                                host: server.hostname.to_string(),
472                                port: server.port,
473                                security,
474                            },
475                            user: if !send_user.is_empty() {
476                                send_user.clone()
477                            } else {
478                                match server.username_pattern {
479                                    UsernamePattern::Email => addr.to_string(),
480                                    UsernamePattern::Emaillocalpart => addr_localpart.clone(),
481                                }
482                            },
483                        })
484                    })
485                    .collect();
486            }
487        } else if let (Some(configured_mail_servers), Some(configured_send_servers)) = (
488            context.get_config(Config::ConfiguredImapServers).await?,
489            context.get_config(Config::ConfiguredSmtpServers).await?,
490        ) {
491            imap = serde_json::from_str(&configured_mail_servers)
492                .context("Failed to parse configured IMAP servers")?;
493            smtp = serde_json::from_str(&configured_send_servers)
494                .context("Failed to parse configured SMTP servers")?;
495        } else {
496            // Load legacy settings storing a single IMAP and single SMTP server.
497            let mail_server = context
498                .get_config(Config::ConfiguredMailServer)
499                .await?
500                .unwrap_or_default();
501            let mail_port = context
502                .get_config_parsed::<u16>(Config::ConfiguredMailPort)
503                .await?
504                .unwrap_or_default();
505
506            let mail_security: Socket = context
507                .get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
508                .await?
509                .and_then(num_traits::FromPrimitive::from_i32)
510                .unwrap_or_default();
511
512            let send_server = context
513                .get_config(Config::ConfiguredSendServer)
514                .await?
515                .context("SMTP server is not configured")?;
516            let send_port = context
517                .get_config_parsed::<u16>(Config::ConfiguredSendPort)
518                .await?
519                .unwrap_or_default();
520            let send_security: Socket = context
521                .get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
522                .await?
523                .and_then(num_traits::FromPrimitive::from_i32)
524                .unwrap_or_default();
525
526            imap = vec![ConfiguredServerLoginParam {
527                connection: ConnectionCandidate {
528                    host: mail_server,
529                    port: mail_port,
530                    security: mail_security.try_into()?,
531                },
532                user: mail_user.clone(),
533            }];
534            smtp = vec![ConfiguredServerLoginParam {
535                connection: ConnectionCandidate {
536                    host: send_server,
537                    port: send_port,
538                    security: send_security.try_into()?,
539                },
540                user: send_user.clone(),
541            }];
542        }
543
544        Ok(Some(ConfiguredLoginParam {
545            addr,
546            imap,
547            imap_user: mail_user,
548            imap_password: mail_pw,
549            smtp,
550            smtp_user: send_user,
551            smtp_password: send_pw,
552            certificate_checks,
553            provider,
554            oauth2,
555        }))
556    }
557
558    pub(crate) async fn save_to_transports_table(
559        self,
560        context: &Context,
561        entered_param: &EnteredLoginParam,
562        timestamp: i64,
563    ) -> Result<()> {
564        save_transport(context, entered_param, &self.into(), timestamp).await?;
565        Ok(())
566    }
567
568    pub(crate) fn from_json(json: &str) -> Result<Self> {
569        let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
570
571        let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
572
573        Ok(ConfiguredLoginParam {
574            addr: json.addr,
575            imap: json.imap,
576            imap_user: json.imap_user,
577            imap_password: json.imap_password,
578            smtp: json.smtp,
579            smtp_user: json.smtp_user,
580            smtp_password: json.smtp_password,
581            provider,
582            certificate_checks: json.certificate_checks,
583            oauth2: json.oauth2,
584        })
585    }
586
587    pub(crate) fn into_json(self) -> Result<String> {
588        let json: ConfiguredLoginParamJson = self.into();
589        Ok(serde_json::to_string(&json)?)
590    }
591
592    pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
593        let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
594        match self.certificate_checks {
595            ConfiguredCertificateChecks::OldAutomatic => {
596                provider_strict_tls.unwrap_or(connected_through_proxy)
597            }
598            ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
599            ConfiguredCertificateChecks::Strict => true,
600            ConfiguredCertificateChecks::AcceptInvalidCertificates
601            | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
602        }
603    }
604}
605
606impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
607    fn from(configured_login_param: ConfiguredLoginParam) -> Self {
608        Self {
609            addr: configured_login_param.addr,
610            imap: configured_login_param.imap,
611            imap_user: configured_login_param.imap_user,
612            imap_password: configured_login_param.imap_password,
613            smtp: configured_login_param.smtp,
614            smtp_user: configured_login_param.smtp_user,
615            smtp_password: configured_login_param.smtp_password,
616            provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
617            certificate_checks: configured_login_param.certificate_checks,
618            oauth2: configured_login_param.oauth2,
619        }
620    }
621}
622
623/// Saves transport to the database.
624/// Returns whether transports are modified.
625pub(crate) async fn save_transport(
626    context: &Context,
627    entered_param: &EnteredLoginParam,
628    configured: &ConfiguredLoginParamJson,
629    add_timestamp: i64,
630) -> Result<bool> {
631    let addr = addr_normalize(&configured.addr);
632    let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
633
634    let mut modified = context
635        .sql
636        .execute(
637            "INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
638             VALUES (?, ?, ?, ?)
639             ON CONFLICT (addr)
640             DO UPDATE SET entered_param=excluded.entered_param,
641                           configured_param=excluded.configured_param,
642                           add_timestamp=excluded.add_timestamp
643             WHERE entered_param != excluded.entered_param
644                 OR configured_param != excluded.configured_param
645                 OR add_timestamp < excluded.add_timestamp",
646            (
647                &addr,
648                serde_json::to_string(entered_param)?,
649                serde_json::to_string(configured)?,
650                add_timestamp,
651            ),
652        )
653        .await?
654        > 0;
655
656    if configured_addr.is_none() {
657        // If there is no transport yet, set the new transport as the primary one
658        context
659            .sql
660            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
661            .await?;
662        modified = true;
663    }
664    Ok(modified)
665}
666
667/// Sends a sync message to synchronize transports across devices.
668pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
669    info!(context, "Sending transport synchronization message.");
670
671    // Synchronize all transport configurations.
672    //
673    // Transport with ID 1 is never synchronized
674    // because it can only be created during initial configuration.
675    // This also guarantees that credentials for the first
676    // transport are never sent in sync messages,
677    // so this is not worse than when not using multi-transport.
678    // If transport ID 1 is reconfigured,
679    // likely because the password has changed,
680    // user has to reconfigure it manually on all devices.
681    let transports = context
682        .sql
683        .query_map_vec(
684            "SELECT entered_param, configured_param, add_timestamp
685             FROM transports WHERE id>1",
686            (),
687            |row| {
688                let entered_json: String = row.get(0)?;
689                let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
690                let configured_json: String = row.get(1)?;
691                let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
692                let timestamp: i64 = row.get(2)?;
693                Ok(TransportData {
694                    configured,
695                    entered,
696                    timestamp,
697                })
698            },
699        )
700        .await?;
701    let removed_transports = context
702        .sql
703        .query_map_vec(
704            "SELECT addr, remove_timestamp FROM removed_transports",
705            (),
706            |row| {
707                let addr: String = row.get(0)?;
708                let timestamp: i64 = row.get(1)?;
709                Ok(RemovedTransportData { addr, timestamp })
710            },
711        )
712        .await?;
713    context
714        .add_sync_item(SyncData::Transports {
715            transports,
716            removed_transports,
717        })
718        .await?;
719    context.scheduler.interrupt_smtp().await;
720
721    Ok(())
722}
723
724/// Process received data for transport synchronization.
725pub(crate) async fn sync_transports(
726    context: &Context,
727    transports: &[TransportData],
728    removed_transports: &[RemovedTransportData],
729) -> Result<()> {
730    let mut modified = false;
731    for TransportData {
732        configured,
733        entered,
734        timestamp,
735    } in transports
736    {
737        modified |= save_transport(context, entered, configured, *timestamp).await?;
738    }
739
740    context
741        .sql
742        .transaction(|transaction| {
743            for RemovedTransportData { addr, timestamp } in removed_transports {
744                modified |= transaction.execute(
745                    "DELETE FROM transports
746                     WHERE addr=? AND add_timestamp<=?",
747                    (addr, timestamp),
748                )? > 0;
749                transaction.execute(
750                    "INSERT INTO removed_transports (addr, remove_timestamp)
751                     VALUES (?, ?)
752                     ON CONFLICT (addr) DO
753                     UPDATE SET remove_timestamp = excluded.remove_timestamp
754                     WHERE excluded.remove_timestamp > remove_timestamp",
755                    (addr, timestamp),
756                )?;
757            }
758            Ok(())
759        })
760        .await?;
761
762    if modified {
763        context.emit_event(EventType::TransportsModified);
764    }
765    Ok(())
766}
767
768/// Adds transport entry to the `transports` table with empty configuration.
769pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
770    context.sql
771        .execute(
772            "INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
773            (
774                addr,
775                serde_json::to_string(&EnteredLoginParam::default())?,
776                format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
777            ),
778        )
779        .await?;
780    Ok(())
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::log::LogExt as _;
787    use crate::provider::get_provider_by_id;
788    use crate::test_utils::TestContext;
789    use crate::tools::time;
790
791    #[test]
792    fn test_configured_certificate_checks_display() {
793        use std::string::ToString;
794
795        assert_eq!(
796            "accept_invalid_certificates".to_string(),
797            ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string()
798        );
799    }
800
801    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
802    async fn test_save_load_login_param() -> Result<()> {
803        let t = TestContext::new().await;
804
805        let param = ConfiguredLoginParam {
806            addr: "alice@example.org".to_string(),
807            imap: vec![ConfiguredServerLoginParam {
808                connection: ConnectionCandidate {
809                    host: "imap.example.com".to_string(),
810                    port: 123,
811                    security: ConnectionSecurity::Starttls,
812                },
813                user: "alice".to_string(),
814            }],
815            imap_user: "".to_string(),
816            imap_password: "foo".to_string(),
817            smtp: vec![ConfiguredServerLoginParam {
818                connection: ConnectionCandidate {
819                    host: "smtp.example.com".to_string(),
820                    port: 456,
821                    security: ConnectionSecurity::Tls,
822                },
823                user: "alice@example.org".to_string(),
824            }],
825            smtp_user: "".to_string(),
826            smtp_password: "bar".to_string(),
827            provider: None,
828            certificate_checks: ConfiguredCertificateChecks::Strict,
829            oauth2: false,
830        };
831
832        param
833            .clone()
834            .save_to_transports_table(&t, &EnteredLoginParam::default(), time())
835            .await?;
836        let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
837        assert_eq!(
838            t.sql
839                .query_get_value::<String>("SELECT configured_param FROM transports", ())
840                .await?
841                .unwrap(),
842            expected_param
843        );
844        assert_eq!(t.is_configured().await?, true);
845        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
846        assert_eq!(param, loaded);
847
848        // Legacy ConfiguredImapCertificateChecks config is ignored
849        t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
850            .await?;
851        assert!(ConfiguredLoginParam::load(&t).await.is_ok());
852
853        // Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
854        let wrong_param = expected_param.replace("Strict", "Stricct");
855        assert_ne!(expected_param, wrong_param);
856        t.sql
857            .execute("UPDATE transports SET configured_param=?", (wrong_param,))
858            .await?;
859        assert!(ConfiguredLoginParam::load(&t).await.is_err());
860
861        Ok(())
862    }
863
864    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
865    async fn test_posteo_alias() -> Result<()> {
866        let t = TestContext::new().await;
867
868        let user = "alice@posteo.de";
869
870        // Alice has old config with "alice@posteo.at" address
871        // and "alice@posteo.de" username.
872        t.set_config(Config::Configured, Some("1")).await?;
873        t.set_config(Config::ConfiguredProvider, Some("posteo"))
874            .await?;
875        t.sql
876            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
877            .await?;
878        t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
879            .await?;
880        t.set_config(Config::ConfiguredMailPort, Some("993"))
881            .await?;
882        t.set_config(Config::ConfiguredMailSecurity, Some("1"))
883            .await?; // TLS
884        t.set_config(Config::ConfiguredMailUser, Some(user)).await?;
885        t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
886            .await?;
887        t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
888            .await?; // Strict
889        t.set_config(Config::ConfiguredSendServer, Some("posteo.de"))
890            .await?;
891        t.set_config(Config::ConfiguredSendPort, Some("465"))
892            .await?;
893        t.set_config(Config::ConfiguredSendSecurity, Some("1"))
894            .await?; // TLS
895        t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
896        t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
897            .await?;
898        t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
899            .await?; // Strict
900        t.set_config(Config::ConfiguredServerFlags, Some("0"))
901            .await?;
902
903        let param = ConfiguredLoginParam {
904            addr: "alice@posteo.at".to_string(),
905            imap: vec![
906                ConfiguredServerLoginParam {
907                    connection: ConnectionCandidate {
908                        host: "posteo.de".to_string(),
909                        port: 993,
910                        security: ConnectionSecurity::Tls,
911                    },
912                    user: user.to_string(),
913                },
914                ConfiguredServerLoginParam {
915                    connection: ConnectionCandidate {
916                        host: "posteo.de".to_string(),
917                        port: 143,
918                        security: ConnectionSecurity::Starttls,
919                    },
920                    user: user.to_string(),
921                },
922            ],
923            imap_user: "alice@posteo.de".to_string(),
924            imap_password: "foobarbaz".to_string(),
925            smtp: vec![
926                ConfiguredServerLoginParam {
927                    connection: ConnectionCandidate {
928                        host: "posteo.de".to_string(),
929                        port: 465,
930                        security: ConnectionSecurity::Tls,
931                    },
932                    user: user.to_string(),
933                },
934                ConfiguredServerLoginParam {
935                    connection: ConnectionCandidate {
936                        host: "posteo.de".to_string(),
937                        port: 587,
938                        security: ConnectionSecurity::Starttls,
939                    },
940                    user: user.to_string(),
941                },
942            ],
943            smtp_user: "alice@posteo.de".to_string(),
944            smtp_password: "foobarbaz".to_string(),
945            provider: get_provider_by_id("posteo"),
946            certificate_checks: ConfiguredCertificateChecks::Strict,
947            oauth2: false,
948        };
949
950        let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
951        assert_eq!(loaded, param);
952
953        migrate_configured_login_param(&t).await;
954        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
955        assert_eq!(loaded, param);
956
957        Ok(())
958    }
959
960    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
961    async fn test_empty_server_list_legacy() -> Result<()> {
962        // Find a provider that does not have server list set.
963        //
964        // There is at least one such provider in the provider database.
965        let (domain, provider) = crate::provider::data::PROVIDER_DATA
966            .iter()
967            .find(|(_domain, provider)| provider.server.is_empty())
968            .unwrap();
969
970        let t = TestContext::new().await;
971
972        let addr = format!("alice@{domain}");
973
974        t.set_config(Config::Configured, Some("1")).await?;
975        t.set_config(Config::ConfiguredProvider, Some(provider.id))
976            .await?;
977        t.sql
978            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
979            .await?;
980        t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
981            .await?;
982        t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
983            .await?; // Strict
984        t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
985            .await?;
986        t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
987            .await?; // Strict
988        t.set_config(Config::ConfiguredServerFlags, Some("0"))
989            .await?;
990
991        let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
992        assert_eq!(loaded.provider, Some(*provider));
993        assert_eq!(loaded.imap.is_empty(), false);
994        assert_eq!(loaded.smtp.is_empty(), false);
995
996        migrate_configured_login_param(&t).await;
997
998        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
999        assert_eq!(loaded.provider, Some(*provider));
1000        assert_eq!(loaded.imap.is_empty(), false);
1001        assert_eq!(loaded.smtp.is_empty(), false);
1002
1003        Ok(())
1004    }
1005
1006    async fn migrate_configured_login_param(t: &TestContext) {
1007        t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
1008        t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
1009        t.sql.run_migrations(t).await.log_err(t).ok();
1010    }
1011
1012    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1013    async fn test_empty_server_list() -> Result<()> {
1014        // Find a provider that does not have server list set.
1015        //
1016        // There is at least one such provider in the provider database.
1017        let (domain, provider) = crate::provider::data::PROVIDER_DATA
1018            .iter()
1019            .find(|(_domain, provider)| provider.server.is_empty())
1020            .unwrap();
1021
1022        let t = TestContext::new().await;
1023
1024        let addr = format!("alice@{domain}");
1025
1026        ConfiguredLoginParam {
1027            addr: addr.clone(),
1028            imap: vec![ConfiguredServerLoginParam {
1029                connection: ConnectionCandidate {
1030                    host: "example.org".to_string(),
1031                    port: 100,
1032                    security: ConnectionSecurity::Tls,
1033                },
1034                user: addr.clone(),
1035            }],
1036            imap_user: addr.clone(),
1037            imap_password: "foobarbaz".to_string(),
1038            smtp: vec![ConfiguredServerLoginParam {
1039                connection: ConnectionCandidate {
1040                    host: "example.org".to_string(),
1041                    port: 100,
1042                    security: ConnectionSecurity::Tls,
1043                },
1044                user: addr.clone(),
1045            }],
1046            smtp_user: addr.clone(),
1047            smtp_password: "foobarbaz".to_string(),
1048            provider: Some(provider),
1049            certificate_checks: ConfiguredCertificateChecks::Automatic,
1050            oauth2: false,
1051        }
1052        .save_to_transports_table(&t, &EnteredLoginParam::default(), time())
1053        .await?;
1054
1055        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
1056        assert_eq!(loaded.provider, Some(*provider));
1057        assert_eq!(loaded.imap.is_empty(), false);
1058        assert_eq!(loaded.smtp.is_empty(), false);
1059        assert_eq!(t.get_configured_provider().await?, Some(*provider));
1060
1061        Ok(())
1062    }
1063}