Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
/// because it itself cannot utilize DNS. See
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
pub async fn ice_servers(context: &Context) -> Result<String> {
if let Some(ref metadata) = *context.metadata.read().await {
if let Some(metadata) = context.metadata.read().await.values().next() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for the future: this may collect all ICE servers, maybe using multiple TURN servers at once makes the calls more reliable.

let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
Ok(ice_servers)
} else {
Expand Down
17 changes: 8 additions & 9 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2969,7 +2969,6 @@ WHERE id=?
)
.await?;

let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();

Expand All @@ -2979,24 +2978,24 @@ WHERE id=?
(),
)?;
}
let mut stmt = t.prepare(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
)?;
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
if !recipients.is_empty() {
Comment thread
hpk42 marked this conversation as resolved.
let mut stmt = t.prepare(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
)?;
let all_recipients = recipients.join(" ");
if let Some(pre_msg) = &rendered_pre_msg {
let row_id = stmt.execute((
&pre_msg.rfc724_mid,
&recipients_chunk,
&all_recipients,
&pre_msg.message,
msg.id,
))?;
row_ids.push(row_id.try_into()?);
}
let row_id = stmt.execute((
&rendered_msg.rfc724_mid,
&recipients_chunk,
&all_recipients,
&rendered_msg.message,
msg.id,
))?;
Expand Down
13 changes: 1 addition & 12 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{get_abs_path, time};
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::transport::{add_pseudo_transport, send_sync_transports};
use crate::{constants, stats};

/// The available configuration keys.
Expand Down Expand Up @@ -629,16 +628,6 @@ impl Context {
self.get_config_bool(Config::MdnsEnabled).await
}

/// Gets the configured provider.
///
/// The provider is determined by the current primary transport.
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
let provider = ConfiguredLoginParam::load(self)
.await?
.and_then(|(_transport_id, param)| param.provider);
Ok(provider)
}

/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
Expand Down
12 changes: 5 additions & 7 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;

// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
// `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;

/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
// Fallback for the maximum number of recipients in SMTP's `RCPT TO:`;
// recipient lists exceeding the limit are sent in chunks.
// Relays advertise their limit via IMAP METADATA
// and in very rare cases the provider db sets `max_smtp_rcpt_to`.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: u32 = 50;

/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
Expand Down
38 changes: 21 additions & 17 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ pub struct InnerContext {
/// <https://datatracker.ietf.org/doc/html/rfc2971>
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,

/// IMAP METADATA.
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
/// IMAP METADATA, per transport id.
pub(crate) metadata: RwLock<BTreeMap<u32, ServerMetadata>>,

/// ID for this `Context` in the current process.
///
Expand Down Expand Up @@ -495,7 +495,7 @@ impl Context {
quota: RwLock::new(BTreeMap::new()),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
metadata: RwLock::new(BTreeMap::new()),
creation_time: tools::Time::now(),
last_error: parking_lot::RwLock::new("".to_string()),
migration_error: parking_lot::RwLock::new(None),
Expand Down Expand Up @@ -575,20 +575,24 @@ impl Context {
self.get_config_bool(Config::IsChatmail).await
}

/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
/// Returns maximum number of recipients a single email can be sent to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<u32> {
let Some((transport_id, param)) = ConfiguredLoginParam::load(self).await? else {
bail!("Not configured");
};
let metadata_limit = self
.metadata
Comment thread
hpk42 marked this conversation as resolved.
.read()
.await
.get(&transport_id)
.and_then(|metadata| metadata.max_smtp_rcpt_to);
if let Some(limit) = metadata_limit {
return Ok(limit);
}
let val = param
.provider
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, u32::from);
Ok(val)
}

Expand Down Expand Up @@ -918,7 +922,7 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);

if let Some(metadata) = &*self.metadata.read().await {
if let Some(metadata) = self.metadata.read().await.values().next() {
if let Some(comment) = &metadata.comment {
res.insert("imap_server_comment", format!("{comment:?}"));
}
Expand Down
42 changes: 32 additions & 10 deletions src/imap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
collections::{BTreeMap, HashMap},
iter::Peekable,
mem::take,
str::FromStr,
sync::atomic::Ordering,
time::{Duration, UNIX_EPOCH},
};
Expand Down Expand Up @@ -136,6 +137,9 @@ pub(crate) struct ServerMetadata {

pub iroh_relay: Option<Url>,

/// Maximum number of recipients for SMTP `RCPT TO:`.
pub max_smtp_rcpt_to: Option<u32>,

/// ICE servers for WebRTC calls.
pub ice_servers: Vec<UnresolvedIceServer>,

Expand Down Expand Up @@ -1359,11 +1363,12 @@ impl Session {
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
let mut lock = context.metadata.write().await;
let transport_id = self.transport_id();

if !self.can_metadata() {
*lock = Some(Default::default());
lock.entry(transport_id).or_default();
}
if let Some(ref mut old_metadata) = *lock {
if let Some(old_metadata) = lock.get_mut(&transport_id) {
let now = time();

// Refresh TURN server credentials if they expire in 12 hours.
Expand Down Expand Up @@ -1413,6 +1418,7 @@ impl Session {
let mut comment = None;
let mut admin = None;
let mut iroh_relay = None;
let mut max_smtp_rcpt_to = None;
let mut ice_servers = None;
let mut ice_servers_expiration_timestamp = 0;

Expand All @@ -1422,7 +1428,7 @@ impl Session {
.get_metadata(
mailbox,
options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients)",
)
.await?;
for m in metadata {
Expand Down Expand Up @@ -1458,6 +1464,18 @@ impl Session {
}
}
}
"/shared/vendor/deltachat/maxsmtprecipients" => {
if let Some(value) = m.value {
if let Ok(limit) = u32::from_str(&value) {
max_smtp_rcpt_to = Some(limit);
} else {
warn!(
context,
"Got invalid maxsmtprecipients metadata: {:?}.", value
);
}
}
}
_ => {}
}
}
Expand All @@ -1469,13 +1487,17 @@ impl Session {
create_fallback_ice_servers()
};

*lock = Some(ServerMetadata {
comment,
admin,
iroh_relay,
ice_servers,
ice_servers_expiration_timestamp,
});
lock.insert(
transport_id,
ServerMetadata {
comment,
admin,
iroh_relay,
max_smtp_rcpt_to,
ice_servers,
ice_servers_expiration_timestamp,
},
);
Ok(())
}

Expand Down
3 changes: 2 additions & 1 deletion src/peer_channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ impl Context {
.metadata
.read()
.await
.as_ref()
.values()
.next()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayUrl::from(relay_url).into())
Expand Down
10 changes: 6 additions & 4 deletions src/receive_imf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,11 +549,13 @@ pub(crate) async fn receive_imf_inner(
// It sometimes happens that a slow server (usually a classical email server)
// receives a message via SMTP,
// but then the connection to the server dies before it sends the OK response.
// In order to handle this case, we delete the SMTP send jobs if we receive our own message via IMAP.
// In order to handle this case, we delete the SMTP send job
// if we receive our own message via IMAP.
//
// Now, messages with long recipient lists are split into multiple SMTP jobs.
// In this case, we only want to delete the SMTP job that was sent to self
// because this is the only chunk we can be sure was sent out.
// Note that messages with long recipient lists are sent out in chunks,
// removing already sent recipients from the job after each chunk.
// Self recipients are added at the end so removing the job
// removes the last chunk which apparently went out fine.
let self_addr = context.get_primary_self_addr().await?;
context
.sql
Expand Down
23 changes: 22 additions & 1 deletion src/smtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,28 @@ pub(crate) async fn send_msg_to_smtp(
)
.collect::<Vec<_>>();

let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await;
let chunk_size = context.get_max_smtp_rcpt_to().await?.max(1);
let mut unsent = recipients_list.as_slice();
let status = loop {
let unsent_len = u32::try_from(unsent.len()).context("Too many SMTP recipients")?;
let split_index = usize::try_from(chunk_size.min(unsent_len))
.context("Failed to convert SMTP chunk size")?;
let (chunk, rest) = unsent.split_at(split_index);
let status = smtp_send(context, chunk, body.as_str(), smtp, Some(msg_id)).await;
if !matches!(status, SendResult::Success) || rest.is_empty() {
break status;
}
let rest_str = rest
.iter()
.map(|a| a.as_ref())
.collect::<Vec<_>>()
.join(" ");
context
.sql
.execute("UPDATE smtp SET recipients=? WHERE id=?", (rest_str, rowid))
.await?;
unsent = rest;
};

match status {
SendResult::Retry => {}
Expand Down
1 change: 0 additions & 1 deletion src/transport/transport_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@ async fn test_empty_server_list() -> Result<()> {
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
assert_eq!(t.get_configured_provider().await?, Some(*provider));

Ok(())
}
Expand Down