mirror of
https://github.com/chatmail/core.git
synced 2026-10-03 03:30:36 +03:00
feat: queue messages for SMTP before encryption
Headers like From and Autocrypt are now added late, right before sending the message over SMTP. This way we advertise the latest list of transports and use the correct From address in the encrypted part even for messages queued while being offline. BCC-self recipients are also added late. For unencrypted messages we only want to send a copy to the sending address, but we don't know the sending address when queueing the message. Adding bcc-self recipients when dequeuing the message also makes it possible to send copies to updated list of relays.
This commit is contained in:
+177
-160
@@ -11,7 +11,6 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
use chrono::TimeZone;
|
||||
use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
|
||||
use humansize::{BINARY, format_size};
|
||||
use mail_builder::mime::MimePart;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -27,26 +26,22 @@ use crate::constants::{
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::{
|
||||
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
|
||||
};
|
||||
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD};
|
||||
use crate::ensure_and_debug_assert_eq;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::key::{DcKey as _, Fingerprint, self_fingerprint};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimefactory::{MimeFactory, QueueSideEffects, QueuedMail, ToBeQueuedMail};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::reaction::broadcast_reactions;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
@@ -336,16 +331,18 @@ impl ChatId {
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
async fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
fn set_selfavatar_timestamp(
|
||||
self,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts
|
||||
SET selfavatar_sent=?
|
||||
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=? AND add_timestamp >= remove_timestamp)",
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
) ?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2776,11 +2773,8 @@ async fn render_mime_message_and_pre_message(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
mimefactory: MimeFactory,
|
||||
) -> Result<(Option<RenderedEmail>, RenderedEmail)> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
|
||||
bcc_self: bool,
|
||||
) -> Result<(Option<ToBeQueuedMail>, ToBeQueuedMail)> {
|
||||
let needs_pre_message = msg.viewtype.has_file()
|
||||
&& mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages
|
||||
&& msg
|
||||
@@ -2797,54 +2791,121 @@ async fn render_mime_message_and_pre_message(
|
||||
|
||||
let mut mimefactory_post_msg = mimefactory.clone();
|
||||
mimefactory_post_msg.set_as_post_message();
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory_post_msg.into_queued_mail(context))
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr.clone(),
|
||||
side_effects,
|
||||
)?;
|
||||
let (queued_msg, side_effects) =
|
||||
Box::pin(mimefactory_post_msg.into_queued_mail(context, bcc_self))
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let mut mimefactory_pre_msg = mimefactory;
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&queued_msg.rfc724_mid);
|
||||
let (queued_pre_msg, pre_side_effects) =
|
||||
Box::pin(mimefactory_pre_msg.into_queued_mail(context))
|
||||
Box::pin(mimefactory_pre_msg.into_queued_mail(context, bcc_self))
|
||||
.await
|
||||
.context("pre-message failed to render")?;
|
||||
let rendered_pre_msg = mimefactory::render_queued_mail(
|
||||
queued_pre_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
pre_side_effects,
|
||||
)?;
|
||||
|
||||
if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD {
|
||||
warn!(
|
||||
context,
|
||||
"Pre-message for message {} is larger than expected: {}.",
|
||||
msg.id,
|
||||
rendered_pre_msg.message.len()
|
||||
);
|
||||
Ok((
|
||||
Some((queued_pre_msg, pre_side_effects)),
|
||||
(queued_msg, side_effects),
|
||||
))
|
||||
} else {
|
||||
let (queued_msg, side_effects) =
|
||||
Box::pin(mimefactory.into_queued_mail(context, bcc_self)).await?;
|
||||
|
||||
Ok((None, (queued_msg, side_effects)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Process side effects and store queued mail.
|
||||
pub(crate) fn enqueue_mail(
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
now: i64,
|
||||
msg_id: MsgId,
|
||||
queued_mail: &QueuedMail,
|
||||
side_effects: Option<&QueueSideEffects>,
|
||||
) -> Result<i64> {
|
||||
if let Some(side_effects) = side_effects {
|
||||
if let Some(last_added_location_timestamp) = side_effects.last_added_location_timestamp {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
(last_added_location_timestamp, side_effects.chat_id),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok((Some(rendered_pre_msg), rendered_msg))
|
||||
} else {
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory.into_queued_mail(context)).await?;
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
side_effects,
|
||||
)?;
|
||||
if side_effects.avatar_is_attached {
|
||||
side_effects
|
||||
.chat_id
|
||||
.set_selfavatar_timestamp(transaction, now)
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
}
|
||||
|
||||
Ok((None, rendered_msg))
|
||||
if let Some(ref sync_ids) = side_effects.sync_ids_to_delete {
|
||||
transaction.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Store mail into queue.
|
||||
let all_recipients = queued_mail.recipients.join(" ");
|
||||
let is_encrypted = queued_mail.encryption.is_encrypted();
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
"
|
||||
INSERT INTO smtp2 (
|
||||
display_name,
|
||||
rfc724_mid,
|
||||
mime,
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
msg_id,
|
||||
recipients,
|
||||
bcc_self,
|
||||
is_encrypted,
|
||||
shared_secret,
|
||||
encryption_fingerprints
|
||||
)
|
||||
VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
",
|
||||
(
|
||||
&queued_mail.display_name,
|
||||
&queued_mail.rfc724_mid,
|
||||
&queued_mail.raw_message,
|
||||
queued_mail.should_attach_pubkey,
|
||||
queued_mail.should_compress,
|
||||
queued_mail.should_sign,
|
||||
msg_id,
|
||||
&all_recipients,
|
||||
queued_mail.bcc_self,
|
||||
is_encrypted,
|
||||
if let mimefactory::QueuedEncryption::Symmetric { ref shared_secret } =
|
||||
queued_mail.encryption
|
||||
{
|
||||
shared_secret
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if let mimefactory::QueuedEncryption::Asymmetric {
|
||||
ref encryption_pubkeys,
|
||||
} = queued_mail.encryption
|
||||
{
|
||||
let res: Vec<String> = encryption_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.dc_fingerprint().hex())
|
||||
.collect();
|
||||
res.join(" ")
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
),
|
||||
)
|
||||
.context("Failed to insert a row into smtp2 table")?;
|
||||
let row_id = transaction.last_insert_rowid();
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
|
||||
@@ -2857,6 +2918,8 @@ async fn render_mime_message_and_pre_message(
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let now = time();
|
||||
|
||||
let cmd = msg.param.get_cmd();
|
||||
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
||||
msg.chat_id
|
||||
@@ -2888,12 +2951,14 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let mut recipients = mimefactory.recipients();
|
||||
let recipients = mimefactory.recipients();
|
||||
debug_assert!(!recipients.iter().any(|s| s.is_empty()));
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out:
|
||||
if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden)
|
||||
// This may happen eg. for groups with only SELF and bcc_self disabled:
|
||||
|| (!context.get_config_bool(Config::BccSelf).await? && recipients.is_empty())
|
||||
|| (!bcc_self && recipients.is_empty())
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
@@ -2906,8 +2971,9 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let (rendered_pre_msg, rendered_msg) =
|
||||
match render_mime_message_and_pre_message(context, msg, mimefactory).await {
|
||||
let is_encrypted = mimefactory.will_be_encrypted();
|
||||
let (queued_pre_msg_pair, queued_msg_pair) =
|
||||
match render_mime_message_and_pre_message(context, msg, mimefactory, bcc_self).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
@@ -2915,29 +2981,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
}
|
||||
}?;
|
||||
|
||||
if let (post_msg, Some(pre_msg)) = (&rendered_msg, &rendered_pre_msg) {
|
||||
info!(
|
||||
context,
|
||||
"Message {} sizes: pre-message: {}; post-message: {}.",
|
||||
msg.id,
|
||||
format_size(pre_msg.message.len(), BINARY),
|
||||
format_size(post_msg.message.len(), BINARY),
|
||||
);
|
||||
if let Some((pre_msg, _)) = &queued_pre_msg_pair {
|
||||
msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone();
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Message {} will be sent in one shot (no pre- and post-message). Size: {}.",
|
||||
msg.id,
|
||||
format_size(rendered_msg.message.len(), BINARY),
|
||||
);
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
smtp::add_self_recipients(context, &mut recipients, rendered_msg.is_encrypted).await?;
|
||||
}
|
||||
let (queued_msg, side_effects) = queued_msg_pair;
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
if needs_encryption && !is_encrypted {
|
||||
let addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let text = stock_str::unencrypted_email(
|
||||
context,
|
||||
@@ -2967,95 +3017,62 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
|
||||
if let Some(last_added_location_timestamp) =
|
||||
rendered_msg.side_effects.last_added_location_timestamp
|
||||
{
|
||||
location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp)
|
||||
.await?;
|
||||
if let Some(ref side_effects) = side_effects {
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
}
|
||||
|
||||
if rendered_msg.side_effects.avatar_is_attached
|
||||
|| rendered_pre_msg
|
||||
.as_ref()
|
||||
.is_some_and(|msg| msg.side_effects.avatar_is_attached)
|
||||
{
|
||||
msg.chat_id
|
||||
.set_selfavatar_timestamp(context, now)
|
||||
.await
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted {
|
||||
if is_encrypted {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
msg.subject.clone_from(&rendered_msg.side_effects.subject);
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.side_effects.sync_ids_to_delete {
|
||||
t.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
if !recipients.is_empty() {
|
||||
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.insert((
|
||||
&pre_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&pre_msg.message,
|
||||
.transaction(|transaction| {
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
transaction.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id);
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut row_ids = Vec::new();
|
||||
if let Some((queued_pre_msg, pre_side_effects)) = queued_pre_msg_pair {
|
||||
let row_id = enqueue_mail(
|
||||
transaction,
|
||||
now,
|
||||
msg.id,
|
||||
&queued_pre_msg,
|
||||
pre_side_effects.as_ref(),
|
||||
)
|
||||
.context("Failed to enqueue pre-message")?;
|
||||
row_ids.push(row_id)
|
||||
}
|
||||
let row_id = stmt.insert((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id);
|
||||
}
|
||||
Ok(row_ids)
|
||||
};
|
||||
context.sql.transaction(trans_fn).await
|
||||
row_ids.push(
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
|
||||
.context("Failed to enqueue message")?,
|
||||
);
|
||||
Ok(row_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends a text message to the given chat.
|
||||
|
||||
Reference in New Issue
Block a user