mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 04:58:47 +03:00
Removing a relay now takes effect immediately: - the profile stops fetching and advertising it, - secondary devices immediately apply the removal through the transport sync, Upgrading removes unpublished relays and triggers keyupdates. BREAKING CHANGE: set_transport_unpublished() is removed: UIs call delete_transport() when the user removes a relay. BREAKING CHANGE: list_transports_ex() and the TransportListEntry type are removed: use list_transports(). BREAKING CHANGE: delete_transport() no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed. BREAKING CHANGE: TransportsModified is now also emitted on the device modifying the transports, not only on devices applying the synced change. Deprecated: DC_STR_PHASING_OUT
784 lines
26 KiB
Rust
784 lines
26 KiB
Rust
//! # Message transport.
|
|
//!
|
|
//! A transport represents a single IMAP+SMTP configuration
|
|
//! that is known to work at least once in the past.
|
|
//!
|
|
//! Transports are stored in the `transports` SQL table.
|
|
//! Each transport is uniquely identified by its email address.
|
|
//! The table stores both the login parameters entered by the user
|
|
//! and configured list of connection candidates.
|
|
|
|
use std::fmt;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
use anyhow::{Context as _, Result, bail, format_err};
|
|
use deltachat_contact_tools::{EmailAddress, addr_normalize};
|
|
use rusqlite::OptionalExtension;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::config::Config;
|
|
use crate::context::Context;
|
|
use crate::ensure_and_debug_assert;
|
|
use crate::events::EventType;
|
|
use crate::keyupdate::{schedule_keyupdate_check, set_current_relays_as_keyupdate_baseline};
|
|
use crate::login_param::EnteredLoginParam;
|
|
use crate::net::load_connection_timestamp;
|
|
use crate::provider::Socket;
|
|
use crate::sql::Sql;
|
|
use crate::sync::{RemovedTransportData, SyncData, TransportData};
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub(crate) enum ConnectionSecurity {
|
|
/// Implicit TLS.
|
|
Tls,
|
|
|
|
/// STARTTLS.
|
|
Starttls,
|
|
|
|
/// Plaintext.
|
|
Plain,
|
|
}
|
|
|
|
impl fmt::Display for ConnectionSecurity {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Tls => write!(f, "tls")?,
|
|
Self::Starttls => write!(f, "starttls")?,
|
|
Self::Plain => write!(f, "plain")?,
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Socket> for ConnectionSecurity {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(socket: Socket) -> Result<Self> {
|
|
match socket {
|
|
Socket::Automatic => Err(format_err!("Socket security is not configured")),
|
|
Socket::Ssl => Ok(Self::Tls),
|
|
Socket::Starttls => Ok(Self::Starttls),
|
|
Socket::Plain => Ok(Self::Plain),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Values saved into `imap_certificate_checks`.
|
|
#[derive(
|
|
Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
|
|
)]
|
|
#[repr(u32)]
|
|
#[strum(serialize_all = "snake_case")]
|
|
pub(crate) enum ConfiguredCertificateChecks {
|
|
/// Accept invalid certificates.
|
|
///
|
|
/// Must not be saved by new versions.
|
|
///
|
|
/// Previous Delta Chat versions before core 1.133.0
|
|
/// stored this in `configured_imap_certificate_checks`
|
|
/// if Automatic configuration
|
|
/// was selected, configuration with strict TLS checks failed
|
|
/// and configuration without strict TLS checks succeeded.
|
|
OldAutomatic = 0,
|
|
|
|
/// Ensure that TLS certificate is valid for the server hostname.
|
|
Strict = 1,
|
|
|
|
/// Accept certificates that are expired, self-signed
|
|
/// or otherwise not valid for the server hostname.
|
|
AcceptInvalidCertificates = 2,
|
|
|
|
/// Accept certificates that are expired, self-signed
|
|
/// or otherwise not valid for the server hostname.
|
|
///
|
|
/// Alias to `AcceptInvalidCertificates` for compatibility.
|
|
AcceptInvalidCertificates2 = 3,
|
|
|
|
/// Apply strict checks to TLS certificates,
|
|
/// unless a legacy-domain override disables them.
|
|
Automatic = 4,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub(crate) struct ConnectionCandidate {
|
|
/// Server hostname or IP address.
|
|
pub host: String,
|
|
|
|
/// Server port.
|
|
pub port: u16,
|
|
|
|
/// Transport layer security.
|
|
pub security: ConnectionSecurity,
|
|
}
|
|
|
|
impl fmt::Display for ConnectionCandidate {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}:{}:{}", self.host, self.port, self.security)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub(crate) struct ConfiguredServerLoginParam {
|
|
pub connection: ConnectionCandidate,
|
|
|
|
/// Username.
|
|
pub user: String,
|
|
}
|
|
|
|
impl fmt::Display for ConfiguredServerLoginParam {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Do not print the username,
|
|
// we do not want it to end up in the logs.
|
|
write!(f, "{}", self.connection)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn prioritize_server_login_params(
|
|
sql: &Sql,
|
|
params: &[ConfiguredServerLoginParam],
|
|
alpn: &str,
|
|
) -> Result<Vec<ConfiguredServerLoginParam>> {
|
|
let mut res: Vec<(Option<i64>, ConfiguredServerLoginParam)> = Vec::with_capacity(params.len());
|
|
for param in params {
|
|
let timestamp = load_connection_timestamp(
|
|
sql,
|
|
alpn,
|
|
¶m.connection.host,
|
|
param.connection.port,
|
|
None,
|
|
)
|
|
.await?;
|
|
res.push((timestamp, param.clone()));
|
|
}
|
|
res.sort_by_key(|(ts, _param)| std::cmp::Reverse(*ts));
|
|
Ok(res.into_iter().map(|(_ts, param)| param).collect())
|
|
}
|
|
|
|
/// Login parameters saved to the database
|
|
/// after successful configuration.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct ConfiguredLoginParam {
|
|
/// `From:` address that was used at the time of configuration.
|
|
pub addr: String,
|
|
|
|
/// List of IMAP candidates to try.
|
|
pub imap: Vec<ConfiguredServerLoginParam>,
|
|
|
|
/// Custom IMAP user.
|
|
///
|
|
/// This overwrites autoconfig if non-empty.
|
|
pub imap_user: String,
|
|
|
|
pub imap_password: String,
|
|
|
|
// IMAP folder to watch.
|
|
//
|
|
// If not stored, should be interpreted as "INBOX".
|
|
// If stored, should be a folder name and not empty.
|
|
pub imap_folder: Option<String>,
|
|
|
|
/// List of SMTP candidates to try.
|
|
pub smtp: Vec<ConfiguredServerLoginParam>,
|
|
|
|
/// Custom SMTP user.
|
|
///
|
|
/// This overwrites autoconfig if non-empty.
|
|
pub smtp_user: String,
|
|
|
|
pub smtp_password: String,
|
|
|
|
/// TLS options: whether to allow invalid certificates and/or
|
|
/// invalid hostnames
|
|
pub certificate_checks: ConfiguredCertificateChecks,
|
|
}
|
|
|
|
/// JSON representation of ConfiguredLoginParam
|
|
/// for the database and sync messages.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub(crate) struct ConfiguredLoginParamJson {
|
|
pub addr: String,
|
|
pub imap: Vec<ConfiguredServerLoginParam>,
|
|
|
|
/// IMAP folder to watch.
|
|
///
|
|
/// Defaults to "INBOX" if unset.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub imap_folder: Option<String>,
|
|
|
|
pub imap_user: String,
|
|
pub imap_password: String,
|
|
pub smtp: Vec<ConfiguredServerLoginParam>,
|
|
pub smtp_user: String,
|
|
pub smtp_password: String,
|
|
|
|
pub certificate_checks: ConfiguredCertificateChecks,
|
|
|
|
/// Deprecated 2026-07, always false
|
|
#[serde(default)]
|
|
pub oauth2: bool,
|
|
}
|
|
|
|
impl fmt::Display for ConfiguredLoginParam {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let certificate_checks = self.certificate_checks;
|
|
if let Ok(parsed_addr) = EmailAddress::new(&self.addr) {
|
|
// Only include the domain.
|
|
write!(f, "***@{}", parsed_addr.domain)?;
|
|
} else {
|
|
// Should not happen, but if the address
|
|
// does not have a distinct domain part,
|
|
// print it as is.
|
|
write!(f, "{}", self.addr)?;
|
|
};
|
|
write!(f, " imap:[")?;
|
|
let mut first = true;
|
|
for imap in &self.imap {
|
|
if !first {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{imap}")?;
|
|
first = false;
|
|
}
|
|
write!(f, "]")?;
|
|
if let Some(folder) = &self.imap_folder {
|
|
write!(f, " folder:{folder:?}")?;
|
|
}
|
|
write!(f, " smtp:[")?;
|
|
let mut first = true;
|
|
for smtp in &self.smtp {
|
|
if !first {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{smtp}")?;
|
|
first = false;
|
|
}
|
|
write!(f, "] cert_{certificate_checks}")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ConfiguredLoginParam {
|
|
/// Load configured account settings from the database.
|
|
///
|
|
/// Returns transport ID and configured parameters
|
|
/// of the current primary transport.
|
|
/// Returns `None` if account is not configured.
|
|
pub(crate) async fn load(context: &Context) -> Result<Option<(u32, Self)>> {
|
|
let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Some((id, json)) = context
|
|
.sql
|
|
.query_row_optional(
|
|
"SELECT id, configured_param FROM transports WHERE addr=?",
|
|
(&self_addr,),
|
|
|row| {
|
|
let id: u32 = row.get(0)?;
|
|
let json: String = row.get(1)?;
|
|
Ok((id, json))
|
|
},
|
|
)
|
|
.await?
|
|
else {
|
|
bail!("Self address {self_addr} doesn't have a corresponding transport");
|
|
};
|
|
Ok(Some((id, Self::from_json(&json)?)))
|
|
}
|
|
|
|
/// Loads configured login parameters for all transports.
|
|
///
|
|
/// Returns a vector of all transport IDs
|
|
/// paired with the configured parameters for the transports.
|
|
pub(crate) async fn load_all(context: &Context) -> Result<Vec<(u32, Self)>> {
|
|
context
|
|
.sql
|
|
.query_map_vec("SELECT id, configured_param FROM transports", (), |row| {
|
|
let id: u32 = row.get(0)?;
|
|
let json: String = row.get(1)?;
|
|
let param = Self::from_json(&json)?;
|
|
Ok((id, param))
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// Loads legacy configured param. Only used for tests and the migration.
|
|
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
|
|
if !context.get_config_bool(Config::Configured).await? {
|
|
return Ok(None);
|
|
}
|
|
|
|
let addr = context
|
|
.get_config(Config::ConfiguredAddr)
|
|
.await?
|
|
.unwrap_or_default()
|
|
.trim()
|
|
.to_string();
|
|
|
|
let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) =
|
|
context
|
|
.get_config_parsed::<i32>(Config::ConfiguredImapCertificateChecks)
|
|
.await?
|
|
{
|
|
num_traits::FromPrimitive::from_i32(certificate_checks)
|
|
.context("Invalid configured_imap_certificate_checks value")?
|
|
} else {
|
|
// This is true for old accounts configured using C core
|
|
// which did not check TLS certificates.
|
|
ConfiguredCertificateChecks::OldAutomatic
|
|
};
|
|
|
|
let send_pw = context
|
|
.get_config(Config::ConfiguredSendPw)
|
|
.await?
|
|
.context("SMTP password is not configured")?;
|
|
let mail_pw = context
|
|
.get_config(Config::ConfiguredMailPw)
|
|
.await?
|
|
.context("IMAP password is not configured")?;
|
|
let imap;
|
|
let smtp;
|
|
|
|
let mail_user = context
|
|
.get_config(Config::ConfiguredMailUser)
|
|
.await?
|
|
.unwrap_or_default();
|
|
let send_user = context
|
|
.get_config(Config::ConfiguredSendUser)
|
|
.await?
|
|
.unwrap_or_default();
|
|
|
|
if let (Some(configured_mail_servers), Some(configured_send_servers)) = (
|
|
context.get_config(Config::ConfiguredImapServers).await?,
|
|
context.get_config(Config::ConfiguredSmtpServers).await?,
|
|
) {
|
|
imap = serde_json::from_str(&configured_mail_servers)
|
|
.context("Failed to parse configured IMAP servers")?;
|
|
smtp = serde_json::from_str(&configured_send_servers)
|
|
.context("Failed to parse configured SMTP servers")?;
|
|
} else {
|
|
// Load legacy settings storing a single IMAP and single SMTP server.
|
|
let mail_server = context
|
|
.get_config(Config::ConfiguredMailServer)
|
|
.await?
|
|
.unwrap_or_default();
|
|
let mail_port = context
|
|
.get_config_parsed::<u16>(Config::ConfiguredMailPort)
|
|
.await?
|
|
.unwrap_or_default();
|
|
|
|
let mail_security: Socket = context
|
|
.get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
|
|
.await?
|
|
.and_then(num_traits::FromPrimitive::from_i32)
|
|
.unwrap_or_default();
|
|
|
|
let send_server = context
|
|
.get_config(Config::ConfiguredSendServer)
|
|
.await?
|
|
.context("SMTP server is not configured")?;
|
|
let send_port = context
|
|
.get_config_parsed::<u16>(Config::ConfiguredSendPort)
|
|
.await?
|
|
.unwrap_or_default();
|
|
let send_security: Socket = context
|
|
.get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
|
|
.await?
|
|
.and_then(num_traits::FromPrimitive::from_i32)
|
|
.unwrap_or_default();
|
|
|
|
imap = vec![ConfiguredServerLoginParam {
|
|
connection: ConnectionCandidate {
|
|
host: mail_server,
|
|
port: mail_port,
|
|
security: mail_security.try_into()?,
|
|
},
|
|
user: mail_user.clone(),
|
|
}];
|
|
smtp = vec![ConfiguredServerLoginParam {
|
|
connection: ConnectionCandidate {
|
|
host: send_server,
|
|
port: send_port,
|
|
security: send_security.try_into()?,
|
|
},
|
|
user: send_user.clone(),
|
|
}];
|
|
}
|
|
|
|
Ok(Some(ConfiguredLoginParam {
|
|
addr,
|
|
imap,
|
|
imap_folder: None,
|
|
imap_user: mail_user,
|
|
imap_password: mail_pw,
|
|
smtp,
|
|
smtp_user: send_user,
|
|
smtp_password: send_pw,
|
|
certificate_checks,
|
|
}))
|
|
}
|
|
|
|
pub(crate) async fn save_to_transports_table(
|
|
self,
|
|
context: &Context,
|
|
entered_param: &EnteredLoginParam,
|
|
timestamp: i64,
|
|
) -> Result<()> {
|
|
save_transport(context, entered_param, &self.into(), timestamp).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn from_json(json: &str) -> Result<Self> {
|
|
let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
|
|
|
|
ensure_and_debug_assert!(
|
|
json.imap_folder
|
|
.as_ref()
|
|
.is_none_or(|folder| !folder.is_empty()),
|
|
"Configured watched folder name cannot be empty"
|
|
);
|
|
|
|
Ok(ConfiguredLoginParam {
|
|
addr: json.addr,
|
|
imap: json.imap,
|
|
imap_folder: json.imap_folder,
|
|
imap_user: json.imap_user,
|
|
imap_password: json.imap_password,
|
|
smtp: json.smtp,
|
|
smtp_user: json.smtp_user,
|
|
smtp_password: json.smtp_password,
|
|
|
|
certificate_checks: json.certificate_checks,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn into_json(self) -> Result<String> {
|
|
let json: ConfiguredLoginParamJson = self.into();
|
|
Ok(serde_json::to_string(&json)?)
|
|
}
|
|
|
|
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> Result<bool> {
|
|
let disable_strict_tls =
|
|
crate::provider::legacy_settings_for_addr(&self.addr)?.disable_strict_tls;
|
|
Ok(match self.certificate_checks {
|
|
ConfiguredCertificateChecks::OldAutomatic if disable_strict_tls => false,
|
|
ConfiguredCertificateChecks::OldAutomatic => connected_through_proxy,
|
|
ConfiguredCertificateChecks::Automatic => !disable_strict_tls,
|
|
ConfiguredCertificateChecks::Strict => true,
|
|
ConfiguredCertificateChecks::AcceptInvalidCertificates
|
|
| ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
|
|
fn from(configured_login_param: ConfiguredLoginParam) -> Self {
|
|
Self {
|
|
addr: configured_login_param.addr,
|
|
imap: configured_login_param.imap,
|
|
imap_user: configured_login_param.imap_user,
|
|
imap_password: configured_login_param.imap_password,
|
|
imap_folder: configured_login_param.imap_folder,
|
|
smtp: configured_login_param.smtp,
|
|
smtp_user: configured_login_param.smtp_user,
|
|
smtp_password: configured_login_param.smtp_password,
|
|
|
|
certificate_checks: configured_login_param.certificate_checks,
|
|
oauth2: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Saves transport to the database.
|
|
/// Returns whether transports are modified.
|
|
pub(crate) async fn save_transport(
|
|
context: &Context,
|
|
entered_param: &EnteredLoginParam,
|
|
configured: &ConfiguredLoginParamJson,
|
|
add_timestamp: i64,
|
|
) -> Result<bool> {
|
|
ensure_and_debug_assert!(
|
|
configured
|
|
.imap_folder
|
|
.as_ref()
|
|
.is_none_or(|folder| !folder.is_empty()),
|
|
"Configured watched folder name cannot be empty"
|
|
);
|
|
|
|
let addr = addr_normalize(&configured.addr);
|
|
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
|
let mut modified = context
|
|
.sql
|
|
.execute(
|
|
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT (addr)
|
|
DO UPDATE SET entered_param=excluded.entered_param,
|
|
configured_param=excluded.configured_param,
|
|
add_timestamp=excluded.add_timestamp
|
|
WHERE entered_param != excluded.entered_param
|
|
OR configured_param != excluded.configured_param
|
|
OR add_timestamp < excluded.add_timestamp",
|
|
(
|
|
&addr,
|
|
serde_json::to_string(entered_param)?,
|
|
serde_json::to_string(configured)?,
|
|
add_timestamp,
|
|
),
|
|
)
|
|
.await?
|
|
> 0;
|
|
|
|
if configured_addr.is_none() {
|
|
// If there is no transport yet, set the new transport as the primary one
|
|
context
|
|
.sql
|
|
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
|
.await?;
|
|
modified = true;
|
|
}
|
|
Ok(modified)
|
|
}
|
|
|
|
/// Sends a sync message to synchronize transports across devices
|
|
/// and emits [`EventType::TransportsModified`].
|
|
pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
|
info!(context, "Sending transport synchronization message.");
|
|
|
|
// Regenerate public key to include all transports.
|
|
context.self_public_key.lock().await.take();
|
|
|
|
// Synchronize all transport configurations.
|
|
//
|
|
// Transport with ID 1 is never synchronized
|
|
// because it can only be created during initial configuration.
|
|
// This also guarantees that credentials for the first
|
|
// transport are never sent in sync messages,
|
|
// so this is not worse than when not using multi-transport.
|
|
// If transport ID 1 is reconfigured,
|
|
// likely because the password has changed,
|
|
// user has to reconfigure it manually on all devices.
|
|
let transports = context
|
|
.sql
|
|
.query_map_vec(
|
|
"SELECT entered_param, configured_param, add_timestamp
|
|
FROM transports WHERE id>1",
|
|
(),
|
|
|row| {
|
|
let entered_json: String = row.get(0)?;
|
|
let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
|
|
let configured_json: String = row.get(1)?;
|
|
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
|
|
let timestamp: i64 = row.get(2)?;
|
|
Ok(TransportData {
|
|
configured,
|
|
entered,
|
|
timestamp,
|
|
is_published: true,
|
|
})
|
|
},
|
|
)
|
|
.await?;
|
|
let removed_transports = context
|
|
.sql
|
|
.query_map_vec(
|
|
"SELECT addr, remove_timestamp FROM removed_transports",
|
|
(),
|
|
|row| {
|
|
let addr: String = row.get(0)?;
|
|
let timestamp: i64 = row.get(1)?;
|
|
Ok(RemovedTransportData { addr, timestamp })
|
|
},
|
|
)
|
|
.await?;
|
|
context
|
|
.add_sync_item(SyncData::Transports {
|
|
transports,
|
|
removed_transports,
|
|
})
|
|
.await?;
|
|
// Schedule the check before interrupting, so the woken SMTP loop sees it.
|
|
schedule_keyupdate_check(context).await?;
|
|
context.scheduler.interrupt_smtp().await;
|
|
context.emit_event(EventType::TransportsModified);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process received data for transport synchronization.
|
|
///
|
|
/// A transport that an older core unpublished
|
|
/// arrives with `is_published: false` and is removed.
|
|
pub(crate) async fn sync_transports(
|
|
context: &Context,
|
|
transports: &[TransportData],
|
|
removed_transports: &[RemovedTransportData],
|
|
) -> Result<()> {
|
|
let mut modified = false;
|
|
for TransportData {
|
|
configured,
|
|
entered,
|
|
timestamp,
|
|
is_published,
|
|
} in transports
|
|
{
|
|
if !is_published {
|
|
continue;
|
|
}
|
|
let removed_later = context
|
|
.sql
|
|
.exists(
|
|
"SELECT COUNT(*) FROM removed_transports WHERE addr=? AND remove_timestamp>=?",
|
|
(&addr_normalize(&configured.addr), timestamp),
|
|
)
|
|
.await?;
|
|
if removed_later {
|
|
// Only a legacy core keeps syncing a transport that was removed here;
|
|
// it must not be resurrected, and removal wins a timestamp tie.
|
|
continue;
|
|
}
|
|
modified |= save_transport(context, entered, configured, *timestamp).await?;
|
|
}
|
|
|
|
let removals: Vec<(String, i64)> = removed_transports
|
|
.iter()
|
|
.map(|removed| (removed.addr.clone(), removed.timestamp))
|
|
.chain(
|
|
transports
|
|
.iter()
|
|
.filter(|data| !data.is_published)
|
|
.map(|data| (addr_normalize(&data.configured.addr), data.timestamp)),
|
|
)
|
|
.collect();
|
|
|
|
let (deleted_ids, reelected) = context
|
|
.sql
|
|
.transaction(|transaction| {
|
|
let mut deleted_ids = Vec::new();
|
|
for (addr, timestamp) in &removals {
|
|
if transport_addrs(transaction)?.len() <= 1 {
|
|
// Removing the last transport would unconfigure the account.
|
|
break;
|
|
}
|
|
deleted_ids.extend(delete_transport_row(transaction, addr, *timestamp)?);
|
|
}
|
|
modified |= !deleted_ids.is_empty();
|
|
|
|
let reelected = maybe_update_sending_transport(transaction)?;
|
|
Ok((deleted_ids, reelected))
|
|
})
|
|
.await?;
|
|
|
|
for transport_id in &deleted_ids {
|
|
purge_transport_caches(context, *transport_id).await;
|
|
}
|
|
|
|
if let Some(new_addr) = reelected {
|
|
info!(context, "Re-elected primary transport {new_addr:?}.");
|
|
context.sql.uncache_raw_config("configured_addr").await;
|
|
modified = true;
|
|
}
|
|
|
|
if modified {
|
|
context.self_public_key.lock().await.take();
|
|
context
|
|
.restart_io_after_fetch
|
|
.store(true, Ordering::Relaxed);
|
|
context.emit_event(EventType::TransportsModified);
|
|
|
|
// Without setting the baseline a restarting SMTP loop
|
|
// would send duplicate keyupdates on every second device.
|
|
// Acceptable gap: a user changing relay setup on two devices concurrently
|
|
// will not trigger a message with the "merged" set of the concurrent changes.
|
|
set_current_relays_as_keyupdate_baseline(context).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the addresses of all transports, newest first.
|
|
pub(crate) fn transport_addrs(transaction: &rusqlite::Transaction) -> Result<Vec<String>> {
|
|
let addrs = transaction
|
|
.prepare("SELECT addr FROM transports ORDER BY add_timestamp DESC, id DESC")?
|
|
.query_map((), |row| row.get(0))?
|
|
.collect::<rusqlite::Result<_>>()?;
|
|
Ok(addrs)
|
|
}
|
|
|
|
/// Deletes the transport row unless it is newer than `remove_timestamp`,
|
|
/// records the tombstone, and returns the deleted row's id.
|
|
pub(crate) fn delete_transport_row(
|
|
transaction: &mut rusqlite::Transaction,
|
|
addr: &str,
|
|
remove_timestamp: i64,
|
|
) -> Result<Option<u32>> {
|
|
let deleted_id: Option<u32> = transaction
|
|
.query_row(
|
|
"DELETE FROM transports
|
|
WHERE addr=? AND add_timestamp<=?
|
|
RETURNING id",
|
|
(addr, remove_timestamp),
|
|
|row| row.get(0),
|
|
)
|
|
.optional()?;
|
|
transaction.execute(
|
|
"INSERT INTO removed_transports (addr, remove_timestamp)
|
|
VALUES (?, ?)
|
|
ON CONFLICT (addr) DO
|
|
UPDATE SET remove_timestamp = excluded.remove_timestamp
|
|
WHERE excluded.remove_timestamp > remove_timestamp",
|
|
(addr, remove_timestamp),
|
|
)?;
|
|
Ok(deleted_id)
|
|
}
|
|
|
|
/// Drops the per-process caches of a removed transport.
|
|
pub(crate) async fn purge_transport_caches(context: &Context, transport_id: u32) {
|
|
context.quota.write().await.remove(&transport_id);
|
|
context.metadata.write().await.remove(&transport_id);
|
|
}
|
|
|
|
/// Elects another transport for sending if the current one vanished.
|
|
/// Any remaining transport works and selection is anyway moving
|
|
/// to the authority of the SMTP loop, see <https://github.com/chatmail/core/pull/8619>
|
|
pub(crate) fn maybe_update_sending_transport(
|
|
transaction: &mut rusqlite::Transaction,
|
|
) -> Result<Option<String>> {
|
|
let configured_addr: String = transaction.query_row(
|
|
"SELECT value FROM config WHERE keyname='configured_addr'",
|
|
(),
|
|
|row| row.get(0),
|
|
)?;
|
|
let addrs = transport_addrs(transaction)?;
|
|
if addrs.contains(&configured_addr) {
|
|
return Ok(None);
|
|
}
|
|
let Some(new_addr) = addrs.into_iter().next() else {
|
|
return Ok(None);
|
|
};
|
|
transaction.execute(
|
|
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
|
(&new_addr,),
|
|
)?;
|
|
Ok(Some(new_addr))
|
|
}
|
|
|
|
/// Adds transport entry to the `transports` table with empty configuration.
|
|
pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
|
|
context.sql
|
|
.execute(
|
|
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
|
(
|
|
addr,
|
|
serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
|
|
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
|
),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod transport_tests;
|