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        save_transport(context, entered_param, &self.into(), timestamp).await?;
566        Ok(())
567    }
568
569    pub(crate) fn from_json(json: &str) -> Result<Self> {
570        let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
571
572        let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
573
574        Ok(ConfiguredLoginParam {
575            addr: json.addr,
576            imap: json.imap,
577            imap_user: json.imap_user,
578            imap_password: json.imap_password,
579            smtp: json.smtp,
580            smtp_user: json.smtp_user,
581            smtp_password: json.smtp_password,
582            provider,
583            certificate_checks: json.certificate_checks,
584            oauth2: json.oauth2,
585        })
586    }
587
588    pub(crate) fn into_json(self) -> Result<String> {
589        let json: ConfiguredLoginParamJson = self.into();
590        Ok(serde_json::to_string(&json)?)
591    }
592
593    pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
594        let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
595        match self.certificate_checks {
596            ConfiguredCertificateChecks::OldAutomatic => {
597                provider_strict_tls.unwrap_or(connected_through_proxy)
598            }
599            ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
600            ConfiguredCertificateChecks::Strict => true,
601            ConfiguredCertificateChecks::AcceptInvalidCertificates
602            | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
603        }
604    }
605}
606
607impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
608    fn from(configured_login_param: ConfiguredLoginParam) -> Self {
609        Self {
610            addr: configured_login_param.addr,
611            imap: configured_login_param.imap,
612            imap_user: configured_login_param.imap_user,
613            imap_password: configured_login_param.imap_password,
614            smtp: configured_login_param.smtp,
615            smtp_user: configured_login_param.smtp_user,
616            smtp_password: configured_login_param.smtp_password,
617            provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
618            certificate_checks: configured_login_param.certificate_checks,
619            oauth2: configured_login_param.oauth2,
620        }
621    }
622}
623
624/// Saves transport to the database.
625/// Returns whether transports are modified.
626pub(crate) async fn save_transport(
627    context: &Context,
628    entered_param: &EnteredLoginParam,
629    configured: &ConfiguredLoginParamJson,
630    add_timestamp: i64,
631) -> Result<bool> {
632    let addr = addr_normalize(&configured.addr);
633    let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
634
635    let mut modified = context
636        .sql
637        .execute(
638            "INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
639             VALUES (?, ?, ?, ?)
640             ON CONFLICT (addr)
641             DO UPDATE SET entered_param=excluded.entered_param,
642                           configured_param=excluded.configured_param,
643                           add_timestamp=excluded.add_timestamp
644             WHERE entered_param != excluded.entered_param
645                 OR configured_param != excluded.configured_param
646                 OR add_timestamp < excluded.add_timestamp",
647            (
648                &addr,
649                serde_json::to_string(entered_param)?,
650                serde_json::to_string(configured)?,
651                add_timestamp,
652            ),
653        )
654        .await?
655        > 0;
656
657    if configured_addr.is_none() {
658        // If there is no transport yet, set the new transport as the primary one
659        context
660            .sql
661            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
662            .await?;
663        modified = true;
664    }
665    Ok(modified)
666}
667
668/// Sends a sync message to synchronize transports across devices.
669pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
670    info!(context, "Sending transport synchronization message.");
671
672    // Synchronize all transport configurations.
673    //
674    // Transport with ID 1 is never synchronized
675    // because it can only be created during initial configuration.
676    // This also guarantees that credentials for the first
677    // transport are never sent in sync messages,
678    // so this is not worse than when not using multi-transport.
679    // If transport ID 1 is reconfigured,
680    // likely because the password has changed,
681    // user has to reconfigure it manually on all devices.
682    let transports = context
683        .sql
684        .query_map_vec(
685            "SELECT entered_param, configured_param, add_timestamp
686             FROM transports WHERE id>1",
687            (),
688            |row| {
689                let entered_json: String = row.get(0)?;
690                let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
691                let configured_json: String = row.get(1)?;
692                let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
693                let timestamp: i64 = row.get(2)?;
694                Ok(TransportData {
695                    configured,
696                    entered,
697                    timestamp,
698                })
699            },
700        )
701        .await?;
702    let removed_transports = context
703        .sql
704        .query_map_vec(
705            "SELECT addr, remove_timestamp FROM removed_transports",
706            (),
707            |row| {
708                let addr: String = row.get(0)?;
709                let timestamp: i64 = row.get(1)?;
710                Ok(RemovedTransportData { addr, timestamp })
711            },
712        )
713        .await?;
714    context
715        .add_sync_item(SyncData::Transports {
716            transports,
717            removed_transports,
718        })
719        .await?;
720    context.scheduler.interrupt_smtp().await;
721
722    Ok(())
723}
724
725/// Process received data for transport synchronization.
726pub(crate) async fn sync_transports(
727    context: &Context,
728    transports: &[TransportData],
729    removed_transports: &[RemovedTransportData],
730) -> Result<()> {
731    let mut modified = false;
732    for TransportData {
733        configured,
734        entered,
735        timestamp,
736    } in transports
737    {
738        modified |= save_transport(context, entered, configured, *timestamp).await?;
739    }
740
741    context
742        .sql
743        .transaction(|transaction| {
744            for RemovedTransportData { addr, timestamp } in removed_transports {
745                modified |= transaction.execute(
746                    "DELETE FROM transports
747                     WHERE addr=? AND add_timestamp<=?",
748                    (addr, timestamp),
749                )? > 0;
750                transaction.execute(
751                    "INSERT INTO removed_transports (addr, remove_timestamp)
752                     VALUES (?, ?)
753                     ON CONFLICT (addr) DO
754                     UPDATE SET remove_timestamp = excluded.remove_timestamp
755                     WHERE excluded.remove_timestamp > remove_timestamp",
756                    (addr, timestamp),
757                )?;
758            }
759            Ok(())
760        })
761        .await?;
762
763    if modified {
764        tokio::task::spawn(restart_io_if_running_boxed(context.clone()));
765        context.emit_event(EventType::TransportsModified);
766    }
767    Ok(())
768}
769
770/// Same as `context.restart_io_if_running()`, but `Box::pin`ed and with a `+ Send` bound,
771/// so that it can be called recursively.
772fn restart_io_if_running_boxed(context: Context) -> Pin<Box<dyn Future<Output = ()> + Send>> {
773    Box::pin(async move { context.restart_io_if_running().await })
774}
775
776/// Adds transport entry to the `transports` table with empty configuration.
777pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
778    context.sql
779        .execute(
780            "INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
781            (
782                addr,
783                serde_json::to_string(&EnteredLoginParam::default())?,
784                format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
785            ),
786        )
787        .await?;
788    Ok(())
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794    use crate::log::LogExt as _;
795    use crate::provider::get_provider_by_id;
796    use crate::test_utils::TestContext;
797    use crate::tools::time;
798
799    #[test]
800    fn test_configured_certificate_checks_display() {
801        use std::string::ToString;
802
803        assert_eq!(
804            "accept_invalid_certificates".to_string(),
805            ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string()
806        );
807    }
808
809    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
810    async fn test_save_load_login_param() -> Result<()> {
811        let t = TestContext::new().await;
812
813        let param = ConfiguredLoginParam {
814            addr: "alice@example.org".to_string(),
815            imap: vec![ConfiguredServerLoginParam {
816                connection: ConnectionCandidate {
817                    host: "imap.example.com".to_string(),
818                    port: 123,
819                    security: ConnectionSecurity::Starttls,
820                },
821                user: "alice".to_string(),
822            }],
823            imap_user: "".to_string(),
824            imap_password: "foo".to_string(),
825            smtp: vec![ConfiguredServerLoginParam {
826                connection: ConnectionCandidate {
827                    host: "smtp.example.com".to_string(),
828                    port: 456,
829                    security: ConnectionSecurity::Tls,
830                },
831                user: "alice@example.org".to_string(),
832            }],
833            smtp_user: "".to_string(),
834            smtp_password: "bar".to_string(),
835            provider: None,
836            certificate_checks: ConfiguredCertificateChecks::Strict,
837            oauth2: false,
838        };
839
840        param
841            .clone()
842            .save_to_transports_table(&t, &EnteredLoginParam::default(), time())
843            .await?;
844        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}"#;
845        assert_eq!(
846            t.sql
847                .query_get_value::<String>("SELECT configured_param FROM transports", ())
848                .await?
849                .unwrap(),
850            expected_param
851        );
852        assert_eq!(t.is_configured().await?, true);
853        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
854        assert_eq!(param, loaded);
855
856        // Legacy ConfiguredImapCertificateChecks config is ignored
857        t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
858            .await?;
859        assert!(ConfiguredLoginParam::load(&t).await.is_ok());
860
861        // Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
862        let wrong_param = expected_param.replace("Strict", "Stricct");
863        assert_ne!(expected_param, wrong_param);
864        t.sql
865            .execute("UPDATE transports SET configured_param=?", (wrong_param,))
866            .await?;
867        assert!(ConfiguredLoginParam::load(&t).await.is_err());
868
869        Ok(())
870    }
871
872    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
873    async fn test_posteo_alias() -> Result<()> {
874        let t = TestContext::new().await;
875
876        let user = "alice@posteo.de";
877
878        // Alice has old config with "alice@posteo.at" address
879        // and "alice@posteo.de" username.
880        t.set_config(Config::Configured, Some("1")).await?;
881        t.set_config(Config::ConfiguredProvider, Some("posteo"))
882            .await?;
883        t.sql
884            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
885            .await?;
886        t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
887            .await?;
888        t.set_config(Config::ConfiguredMailPort, Some("993"))
889            .await?;
890        t.set_config(Config::ConfiguredMailSecurity, Some("1"))
891            .await?; // TLS
892        t.set_config(Config::ConfiguredMailUser, Some(user)).await?;
893        t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
894            .await?;
895        t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
896            .await?; // Strict
897        t.set_config(Config::ConfiguredSendServer, Some("posteo.de"))
898            .await?;
899        t.set_config(Config::ConfiguredSendPort, Some("465"))
900            .await?;
901        t.set_config(Config::ConfiguredSendSecurity, Some("1"))
902            .await?; // TLS
903        t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
904        t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
905            .await?;
906        t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
907            .await?; // Strict
908        t.set_config(Config::ConfiguredServerFlags, Some("0"))
909            .await?;
910
911        let param = ConfiguredLoginParam {
912            addr: "alice@posteo.at".to_string(),
913            imap: vec![
914                ConfiguredServerLoginParam {
915                    connection: ConnectionCandidate {
916                        host: "posteo.de".to_string(),
917                        port: 993,
918                        security: ConnectionSecurity::Tls,
919                    },
920                    user: user.to_string(),
921                },
922                ConfiguredServerLoginParam {
923                    connection: ConnectionCandidate {
924                        host: "posteo.de".to_string(),
925                        port: 143,
926                        security: ConnectionSecurity::Starttls,
927                    },
928                    user: user.to_string(),
929                },
930            ],
931            imap_user: "alice@posteo.de".to_string(),
932            imap_password: "foobarbaz".to_string(),
933            smtp: vec![
934                ConfiguredServerLoginParam {
935                    connection: ConnectionCandidate {
936                        host: "posteo.de".to_string(),
937                        port: 465,
938                        security: ConnectionSecurity::Tls,
939                    },
940                    user: user.to_string(),
941                },
942                ConfiguredServerLoginParam {
943                    connection: ConnectionCandidate {
944                        host: "posteo.de".to_string(),
945                        port: 587,
946                        security: ConnectionSecurity::Starttls,
947                    },
948                    user: user.to_string(),
949                },
950            ],
951            smtp_user: "alice@posteo.de".to_string(),
952            smtp_password: "foobarbaz".to_string(),
953            provider: get_provider_by_id("posteo"),
954            certificate_checks: ConfiguredCertificateChecks::Strict,
955            oauth2: false,
956        };
957
958        let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
959        assert_eq!(loaded, param);
960
961        migrate_configured_login_param(&t).await;
962        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
963        assert_eq!(loaded, param);
964
965        Ok(())
966    }
967
968    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
969    async fn test_empty_server_list_legacy() -> Result<()> {
970        // Find a provider that does not have server list set.
971        //
972        // There is at least one such provider in the provider database.
973        let (domain, provider) = crate::provider::data::PROVIDER_DATA
974            .iter()
975            .find(|(_domain, provider)| provider.server.is_empty())
976            .unwrap();
977
978        let t = TestContext::new().await;
979
980        let addr = format!("alice@{domain}");
981
982        t.set_config(Config::Configured, Some("1")).await?;
983        t.set_config(Config::ConfiguredProvider, Some(provider.id))
984            .await?;
985        t.sql
986            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
987            .await?;
988        t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
989            .await?;
990        t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
991            .await?; // Strict
992        t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
993            .await?;
994        t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
995            .await?; // Strict
996        t.set_config(Config::ConfiguredServerFlags, Some("0"))
997            .await?;
998
999        let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
1000        assert_eq!(loaded.provider, Some(*provider));
1001        assert_eq!(loaded.imap.is_empty(), false);
1002        assert_eq!(loaded.smtp.is_empty(), false);
1003
1004        migrate_configured_login_param(&t).await;
1005
1006        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
1007        assert_eq!(loaded.provider, Some(*provider));
1008        assert_eq!(loaded.imap.is_empty(), false);
1009        assert_eq!(loaded.smtp.is_empty(), false);
1010
1011        Ok(())
1012    }
1013
1014    async fn migrate_configured_login_param(t: &TestContext) {
1015        t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
1016        t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
1017        t.sql.run_migrations(t).await.log_err(t).ok();
1018    }
1019
1020    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1021    async fn test_empty_server_list() -> Result<()> {
1022        // Find a provider that does not have server list set.
1023        //
1024        // There is at least one such provider in the provider database.
1025        let (domain, provider) = crate::provider::data::PROVIDER_DATA
1026            .iter()
1027            .find(|(_domain, provider)| provider.server.is_empty())
1028            .unwrap();
1029
1030        let t = TestContext::new().await;
1031
1032        let addr = format!("alice@{domain}");
1033
1034        ConfiguredLoginParam {
1035            addr: addr.clone(),
1036            imap: vec![ConfiguredServerLoginParam {
1037                connection: ConnectionCandidate {
1038                    host: "example.org".to_string(),
1039                    port: 100,
1040                    security: ConnectionSecurity::Tls,
1041                },
1042                user: addr.clone(),
1043            }],
1044            imap_user: addr.clone(),
1045            imap_password: "foobarbaz".to_string(),
1046            smtp: vec![ConfiguredServerLoginParam {
1047                connection: ConnectionCandidate {
1048                    host: "example.org".to_string(),
1049                    port: 100,
1050                    security: ConnectionSecurity::Tls,
1051                },
1052                user: addr.clone(),
1053            }],
1054            smtp_user: addr.clone(),
1055            smtp_password: "foobarbaz".to_string(),
1056            provider: Some(provider),
1057            certificate_checks: ConfiguredCertificateChecks::Automatic,
1058            oauth2: false,
1059        }
1060        .save_to_transports_table(&t, &EnteredLoginParam::default(), time())
1061        .await?;
1062
1063        let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
1064        assert_eq!(loaded.provider, Some(*provider));
1065        assert_eq!(loaded.imap.is_empty(), false);
1066        assert_eq!(loaded.smtp.is_empty(), false);
1067        assert_eq!(t.get_configured_provider().await?, Some(*provider));
1068
1069        Ok(())
1070    }
1071}