mirror of
https://github.com/chatmail/core.git
synced 2026-05-10 10:26:30 +03:00
Compare commits
2 Commits
link2xt/pq
...
hpk/per-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f1c02d89f | ||
|
|
5e7fcdac8f |
88
Cargo.lock
generated
88
Cargo.lock
generated
@@ -2608,25 +2608,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
@@ -3276,16 +3257,6 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kem"
|
||||
version = "0.3.0-pre.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3499,35 +3470,6 @@ dependencies = [
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-dsa"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"hybrid-array 0.3.1",
|
||||
"num-traits",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"signature",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-kem"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
|
||||
dependencies = [
|
||||
"hybrid-array 0.2.3",
|
||||
"kem",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
@@ -4263,8 +4205,6 @@ dependencies = [
|
||||
"k256",
|
||||
"log",
|
||||
"md-5",
|
||||
"ml-dsa",
|
||||
"ml-kem",
|
||||
"nom 8.0.0",
|
||||
"num-bigint-dig",
|
||||
"num-traits",
|
||||
@@ -4283,7 +4223,6 @@ dependencies = [
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"slh-dsa",
|
||||
"smallvec",
|
||||
"snafu",
|
||||
"twofish",
|
||||
@@ -5748,25 +5687,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slh-dsa"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"hmac",
|
||||
"hybrid-array 0.3.1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"typenum",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -7505,9 +7425,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
@@ -7515,9 +7435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -78,7 +78,7 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.19.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
|
||||
@@ -43,12 +43,7 @@ ignore = [
|
||||
# hickory-proto 0.25.2 quadratic complexity issue.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
|
||||
"RUSTSEC-2026-0119",
|
||||
|
||||
# Timing side channel in ml-dsa dependency of rPGP.
|
||||
# We enable PQC for encryption rather than signatures.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
|
||||
"RUSTSEC-2025-0144",
|
||||
"RUSTSEC-2026-0119"
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -67,7 +62,6 @@ skip = [
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
|
||||
@@ -2954,7 +2954,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();
|
||||
|
||||
@@ -2968,12 +2967,12 @@ WHERE id=?
|
||||
"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() {
|
||||
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,
|
||||
))?;
|
||||
@@ -2981,7 +2980,7 @@ WHERE id=?
|
||||
}
|
||||
let row_id = stmt.execute((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&all_recipients,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
|
||||
@@ -204,9 +204,6 @@ pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
|
||||
// `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;
|
||||
|
||||
/// 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
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::chat::{ChatId, get_chat_cnt};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
|
||||
use crate::constants::{DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
@@ -587,23 +587,6 @@ 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?
|
||||
.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,
|
||||
);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Does a single round of fetching from IMAP and returns.
|
||||
///
|
||||
/// Can be used even if I/O is currently stopped.
|
||||
|
||||
17
src/imap.rs
17
src/imap.rs
@@ -1503,7 +1503,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 {
|
||||
@@ -1539,6 +1539,21 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
"/shared/vendor/deltachat/maxsmtprecipients" => {
|
||||
if let Some(value) = m.value.and_then(|v| v.parse::<u32>().ok()) {
|
||||
let transport_id = self.transport_id();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE transports \
|
||||
SET max_smtp_rcpt_to=? WHERE id=?",
|
||||
(value, transport_id),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,11 +570,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
/// Creates new fingerprint.
|
||||
///
|
||||
/// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys.
|
||||
/// Creates new 160-bit (20 bytes) fingerprint.
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert!(v.len() == 20 || v.len() == 32);
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ impl MimeMessage {
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
let expected_sender_fingerprint: Option<String>;
|
||||
|
||||
let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await {
|
||||
let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await {
|
||||
Ok(Some((mut msg, expected_sender_fp))) => {
|
||||
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||
|
||||
|
||||
17
src/pgp.rs
17
src/pgp.rs
@@ -847,21 +847,4 @@ mod tests {
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pqc() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let pqc = &tcm.pqc().await;
|
||||
|
||||
let pqc_received_message = tcm.send_recv_accept(alice, pqc, "Hi!").await;
|
||||
let pqc_chat_id = pqc_received_message.chat_id;
|
||||
let pqc_sent = pqc.send_text(pqc_chat_id, "Hello back!").await;
|
||||
|
||||
let alice_rcvd = alice.recv_msg(&pqc_sent).await;
|
||||
assert_eq!(alice_rcvd.text, "Hello back!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
155
src/smtp.rs
155
src/smtp.rs
@@ -2,6 +2,8 @@
|
||||
|
||||
mod connect;
|
||||
pub mod send;
|
||||
#[cfg(test)]
|
||||
mod chunking_tests;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail, format_err};
|
||||
use async_smtp::response::{Category, Code, Detail};
|
||||
@@ -10,6 +12,7 @@ use tokio::task;
|
||||
|
||||
use crate::chat::{ChatId, add_info_msg_with_cmd};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -34,6 +37,9 @@ pub(crate) struct Smtp {
|
||||
/// Email address we are sending from.
|
||||
from: Option<EmailAddress>,
|
||||
|
||||
/// Transport used for the current connection.
|
||||
transport_id: Option<u32>,
|
||||
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
@@ -60,6 +66,7 @@ impl Smtp {
|
||||
task::spawn(async move { transport.quit().await });
|
||||
}
|
||||
self.last_success = None;
|
||||
self.transport_id = None;
|
||||
}
|
||||
|
||||
/// Return true if smtp was connected but is not known to
|
||||
@@ -89,9 +96,10 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.connectivity.set_connecting(context);
|
||||
let (_transport_id, lp) = ConfiguredLoginParam::load(context)
|
||||
let (transport_id, lp) = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
self.transport_id = Some(transport_id);
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
self.connect(
|
||||
context,
|
||||
@@ -165,6 +173,7 @@ impl Smtp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SendResult {
|
||||
/// Message was sent successfully.
|
||||
Success,
|
||||
@@ -176,13 +185,36 @@ pub(crate) enum SendResult {
|
||||
Retry,
|
||||
}
|
||||
|
||||
pub(crate) trait SmtpSender: Send {
|
||||
fn send_chunk<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
recipients: &'a [async_smtp::EmailAddress],
|
||||
body: &'a str,
|
||||
) -> futures::future::BoxFuture<'a, SendResult>;
|
||||
}
|
||||
|
||||
struct RealSmtpSender<'a> {
|
||||
smtp: &'a mut Smtp,
|
||||
}
|
||||
|
||||
impl SmtpSender for RealSmtpSender<'_> {
|
||||
fn send_chunk<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
recipients: &'a [async_smtp::EmailAddress],
|
||||
body: &'a str,
|
||||
) -> futures::future::BoxFuture<'a, SendResult> {
|
||||
Box::pin(smtp_send(context, recipients, body, self.smtp))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to send a message.
|
||||
pub(crate) async fn smtp_send(
|
||||
context: &Context,
|
||||
recipients: &[async_smtp::EmailAddress],
|
||||
message: &str,
|
||||
smtp: &mut Smtp,
|
||||
msg_id: Option<MsgId>,
|
||||
) -> SendResult {
|
||||
if recipients.is_empty() {
|
||||
return SendResult::Success;
|
||||
@@ -310,25 +342,6 @@ pub(crate) async fn smtp_send(
|
||||
Ok(()) => SendResult::Success,
|
||||
};
|
||||
|
||||
if let SendResult::Failure(err) = &status
|
||||
&& let Some(msg_id) = msg_id
|
||||
{
|
||||
// We couldn't send the message, so mark it as failed
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
@@ -406,7 +419,40 @@ 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 transport_id = smtp
|
||||
.transport_id
|
||||
.context("SMTP not connected to a transport")?;
|
||||
let chunk_size = max_smtp_rcpt_to(context, transport_id).await?;
|
||||
|
||||
let mut sender = RealSmtpSender { smtp };
|
||||
let (status, start_idx) = send_smtp_chunks(
|
||||
context,
|
||||
&recipients_list,
|
||||
body.as_str(),
|
||||
chunk_size,
|
||||
&mut sender,
|
||||
)
|
||||
.await;
|
||||
|
||||
let unsent_saved = start_idx < recipients_list.len();
|
||||
if let Some(unsent) = recipients_list.get(start_idx..)
|
||||
&& !unsent.is_empty()
|
||||
{
|
||||
let unsent_str: String = unsent
|
||||
.iter()
|
||||
.map(|a| a.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp SET recipients=? WHERE id=?",
|
||||
(unsent_str, rowid),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
match status {
|
||||
SendResult::Retry => {}
|
||||
@@ -455,10 +501,15 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.await?;
|
||||
};
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await?;
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
message::set_msg_failed(context, &mut msg, &err.to_string()).await?;
|
||||
}
|
||||
if !unsent_saved {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -470,10 +521,39 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SendResult::Failure(err) => Err(format_err!("{err}")),
|
||||
SendResult::Failure(err) => {
|
||||
if unsent_saved {
|
||||
Err(format_err!("Retry"))
|
||||
} else {
|
||||
Err(format_err!("{err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn max_smtp_rcpt_to(context: &Context, transport_id: u32) -> Result<usize> {
|
||||
let limit = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT max_smtp_rcpt_to FROM transports WHERE id=?",
|
||||
(transport_id,),
|
||||
|row| row.get::<_, u32>(0),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
if limit > 0 {
|
||||
return Ok(limit as usize);
|
||||
}
|
||||
|
||||
let val = context
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub(crate) async fn msg_has_pending_smtp_job(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
@@ -600,7 +680,7 @@ async fn send_mdn_rfc724_mid(
|
||||
})
|
||||
.collect();
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
match smtp_send(context, &recipients, &body, smtp).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
@@ -722,3 +802,22 @@ pub(crate) async fn add_self_recipients(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn send_smtp_chunks(
|
||||
context: &Context,
|
||||
recipients: &[async_smtp::EmailAddress],
|
||||
body: &str,
|
||||
chunk_size: usize,
|
||||
sender: &mut (dyn SmtpSender + Send),
|
||||
) -> (SendResult, usize) {
|
||||
for (i, chunk) in recipients.chunks(chunk_size).enumerate() {
|
||||
let status = sender.send_chunk(context, chunk, body).await;
|
||||
match status {
|
||||
SendResult::Success => continue,
|
||||
SendResult::Failure(_) => return (status, (i + 1) * chunk_size),
|
||||
SendResult::Retry => return (status, i * chunk_size),
|
||||
}
|
||||
}
|
||||
(SendResult::Success, recipients.len())
|
||||
}
|
||||
|
||||
102
src/smtp/chunking_tests.rs
Normal file
102
src/smtp/chunking_tests.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use crate::smtp::{send_smtp_chunks, SendResult, SmtpSender};
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::context::Context;
|
||||
use anyhow::Result;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
|
||||
/// Result the mock should return on the designated call.
|
||||
enum MockFailure {
|
||||
Transient,
|
||||
Permanent,
|
||||
}
|
||||
|
||||
struct MockSmtpSender {
|
||||
call_count: usize,
|
||||
fail_on_call: Option<(usize, MockFailure)>,
|
||||
}
|
||||
|
||||
impl SmtpSender for MockSmtpSender {
|
||||
fn send_chunk<'a>(
|
||||
&'a mut self,
|
||||
_context: &'a Context,
|
||||
_recipients: &'a [async_smtp::EmailAddress],
|
||||
_body: &'a str,
|
||||
) -> BoxFuture<'a, SendResult> {
|
||||
self.call_count += 1;
|
||||
let count = self.call_count;
|
||||
let fail_on = self.fail_on_call.as_ref().map(|(n, _)| *n);
|
||||
let is_permanent = matches!(
|
||||
self.fail_on_call,
|
||||
Some((_, MockFailure::Permanent))
|
||||
);
|
||||
async move {
|
||||
if fail_on == Some(count) {
|
||||
if is_permanent {
|
||||
return SendResult::Failure(
|
||||
anyhow::format_err!("permanent error"),
|
||||
);
|
||||
}
|
||||
return SendResult::Retry;
|
||||
}
|
||||
SendResult::Success
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_smtp_chunks() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let recipients: Vec<_> = ["r1@ex.org", "r2@ex.org", "r3@ex.org", "r4@ex.org", "r5@ex.org"]
|
||||
.iter()
|
||||
.map(|a| async_smtp::EmailAddress::new(a.to_string()).unwrap())
|
||||
.collect();
|
||||
|
||||
// All chunks succeed.
|
||||
let mut sender = MockSmtpSender { call_count: 0, fail_on_call: None };
|
||||
let (status, processed) =
|
||||
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
|
||||
assert!(matches!(status, SendResult::Success));
|
||||
assert_eq!(processed, 5);
|
||||
assert_eq!(sender.call_count, 3); // chunks: [2, 2, 1]
|
||||
|
||||
// Second chunk gets a transient error, only first chunk's recipients are processed.
|
||||
let mut sender =
|
||||
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Transient)) };
|
||||
let (status, processed) =
|
||||
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
|
||||
assert!(matches!(status, SendResult::Retry));
|
||||
assert_eq!(processed, 2);
|
||||
assert_eq!(sender.call_count, 2);
|
||||
|
||||
// Last chunk gets a transient error, first two chunks' recipients are processed.
|
||||
let mut sender =
|
||||
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Transient)) };
|
||||
let (status, processed) =
|
||||
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
|
||||
assert!(matches!(status, SendResult::Retry));
|
||||
assert_eq!(processed, 4);
|
||||
assert_eq!(sender.call_count, 3);
|
||||
|
||||
// Second chunk gets a permanent error; processed includes the failed chunk.
|
||||
let mut sender =
|
||||
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Permanent)) };
|
||||
let (status, processed) =
|
||||
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
|
||||
assert!(matches!(status, SendResult::Failure(_)));
|
||||
assert_eq!(processed, 4);
|
||||
assert_eq!(sender.call_count, 2);
|
||||
|
||||
// Last chunk gets a permanent error; processed includes the failed chunk.
|
||||
let mut sender =
|
||||
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Permanent)) };
|
||||
let (status, processed) =
|
||||
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
|
||||
assert!(matches!(status, SendResult::Failure(_)));
|
||||
assert_eq!(processed, 6); // capped at (i+1)*chunk_size, may exceed len
|
||||
assert_eq!(sender.call_count, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2385,6 +2385,15 @@ UPDATE msgs SET state=19 WHERE state=24; -- Change OutPreparing to OutFailed.
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 153)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE transports ADD COLUMN max_smtp_rcpt_to INTEGER NOT NULL DEFAULT 0",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -137,17 +137,6 @@ impl TestContextManager {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a new "device" with a preconfigured v6 PQC key.
|
||||
pub async fn pqc(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
.with_key_pair(pqc_keypair())
|
||||
.with_address("pqc@example.org".to_string())
|
||||
.with_id_offset(7000)
|
||||
.with_log_sink(self.log_sink.clone())
|
||||
.build(Some(&mut self.used_names))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a new unconfigured test account.
|
||||
pub async fn unconfigured(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
@@ -315,9 +304,6 @@ impl TestContextManager {
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<SignedSecretKey>,
|
||||
|
||||
/// Email address.
|
||||
address: Option<String>,
|
||||
|
||||
/// Log sink if set.
|
||||
///
|
||||
/// If log sink is not set,
|
||||
@@ -342,7 +328,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(alice_keypair())`.
|
||||
pub fn configure_alice(self) -> Self {
|
||||
self.with_key_pair(alice_keypair())
|
||||
.with_address("alice@example.org".to_string())
|
||||
}
|
||||
|
||||
/// Configures as bob@example.net with fixed secret key.
|
||||
@@ -350,7 +335,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(bob_keypair())`.
|
||||
pub fn configure_bob(self) -> Self {
|
||||
self.with_key_pair(bob_keypair())
|
||||
.with_address("bob@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as charlie@example.net with fixed secret key.
|
||||
@@ -358,7 +342,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(charlie_keypair())`.
|
||||
pub fn configure_charlie(self) -> Self {
|
||||
self.with_key_pair(charlie_keypair())
|
||||
.with_address("charlie@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as dom@example.net with fixed secret key.
|
||||
@@ -366,7 +349,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(dom_keypair())`.
|
||||
pub fn configure_dom(self) -> Self {
|
||||
self.with_key_pair(dom_keypair())
|
||||
.with_address("dom@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as elena@example.net with fixed secret key.
|
||||
@@ -374,7 +356,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(elena_keypair())`.
|
||||
pub fn configure_elena(self) -> Self {
|
||||
self.with_key_pair(elena_keypair())
|
||||
.with_address("elena@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as fiona@example.net with fixed secret key.
|
||||
@@ -382,7 +363,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(fiona_keypair())`.
|
||||
pub fn configure_fiona(self) -> Self {
|
||||
self.with_key_pair(fiona_keypair())
|
||||
.with_address("fiona@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures the new [`TestContext`] with the provided [`SignedSecretKey`].
|
||||
@@ -394,12 +374,6 @@ impl TestContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets email address.
|
||||
pub fn with_address(mut self, address: String) -> Self {
|
||||
self.address = Some(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches a [`LogSink`] to this [`TestContext`].
|
||||
///
|
||||
/// This is useful when using multiple [`TestContext`] instances in one test: it allows
|
||||
@@ -422,7 +396,16 @@ impl TestContextBuilder {
|
||||
/// Builds the [`TestContext`].
|
||||
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
|
||||
if let Some(key_pair) = self.key_pair {
|
||||
let addr = self.address.expect("Address is not set").clone();
|
||||
let userid = {
|
||||
let public_key = key_pair.to_public_key();
|
||||
let id_bstr = public_key.details.users.first().unwrap().id.id();
|
||||
String::from_utf8(id_bstr.to_vec()).unwrap()
|
||||
};
|
||||
let addr = mailparse::addrparse(&userid)
|
||||
.unwrap()
|
||||
.extract_single_info()
|
||||
.unwrap()
|
||||
.addr;
|
||||
let name = EmailAddress::new(&addr).unwrap().local;
|
||||
|
||||
let mut unused_name = name.clone();
|
||||
@@ -1437,13 +1420,6 @@ pub fn fiona_keypair() -> SignedSecretKey {
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap()
|
||||
}
|
||||
|
||||
/// Loads a pre-generated v6 PQC keypair from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn pqc_keypair() -> SignedSecretKey {
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/pqc-secret.asc")).unwrap()
|
||||
}
|
||||
|
||||
/// Utility to help wait for and retrieve events.
|
||||
///
|
||||
/// This buffers the events in order they are emitted. This allows consuming events in
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
Reference in New Issue
Block a user