Skip to content

Queue messages for SMTP before encryption - #8619

Open
link2xt wants to merge 6 commits into
mainfrom
link2xt/late-encryption
Open

Queue messages for SMTP before encryption#8619
link2xt wants to merge 6 commits into
mainfrom
link2xt/late-encryption

Conversation

@link2xt

@link2xt link2xt commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Part of #8607, this removes the code that deletes the whole queue when the sending relay is changed, and makes queueing the mail independent of the sending address. Follow-up to #8345 in some way, this now uses QueuedMail for real.

There is a python test that was previously failing, deleting the current transport while having messages queued.

Large number of lines added is because of better types (new QueuedEncryption separate from mimefactory::Encryption) and duplicated smtp2 table with documentation.

Left for follow-up PRs:

  • Store side effects and the mail itself in a single transaction in chat::enqueue_mail. This needs refactoring location streaming code etc. that is unrelated.
  • Moving QueuedMail, QueuedEncryption and related functions from mimefactory to a separate module.
  • Cycling through transports in SMTP loop if connection to the first one fails and connecting to the last used one instead of ConfiguredAddr.

Note re performance of encryption: maybe it even makes UI more responsive because the message bubble can appear on the sending side faster as the message does not need to be encrypted yet.

Commit message 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.

@link2xt
link2xt force-pushed the link2xt/late-encryption branch 2 times, most recently from 026e72d to ce56911 Compare August 25, 2026 17:11
@link2xt
link2xt changed the base branch from main to link2xt/create_send_msg_jobs-row_id August 25, 2026 17:14
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 2 times, most recently from d688b11 to 6809578 Compare August 25, 2026 20:23
@link2xt
link2xt force-pushed the link2xt/create_send_msg_jobs-row_id branch from e657a72 to a67a5b0 Compare August 26, 2026 14:36
Base automatically changed from link2xt/create_send_msg_jobs-row_id to main August 26, 2026 20:22
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 3 times, most recently from 756d28c to 3d72fe8 Compare September 1, 2026 17:25
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 8 times, most recently from b838513 to bdfd0d4 Compare September 9, 2026 05:43
@link2xt
link2xt changed the base branch from main to link2xt/refactor-ref-vec September 9, 2026 06:13
@link2xt
link2xt added this pull request to stack #8677 September 9, 2026 06:13
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 2 times, most recently from 958c526 to fe41960 Compare September 9, 2026 07:55
Base automatically changed from link2xt/refactor-ref-vec to main September 9, 2026 08:34
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 2 times, most recently from 8a42d9b to b9bba12 Compare September 9, 2026 08:37
@link2xt
link2xt removed this pull request from stack #8677 September 9, 2026 08:38
@link2xt
link2xt force-pushed the link2xt/late-encryption branch 2 times, most recently from 3e8fdaa to eb1c34c Compare September 9, 2026 10:11
@link2xt
link2xt force-pushed the link2xt/late-encryption branch from eb1c34c to 7eb324f Compare September 9, 2026 10:26
@link2xt link2xt changed the title WIP: Late encryption Queue messages for SMTP before encryption Sep 9, 2026
@link2xt
link2xt marked this pull request as ready for review September 9, 2026 10:27
@link2xt
link2xt requested review from Hocuri and j-g00da September 10, 2026 09:02
Comment thread src/chat.rs
Comment on lines +2776 to +2779
) -> Result<(
Option<(QueuedMail, Option<QueueSideEffects>)>,
(QueuedMail, Option<QueueSideEffects>),
)> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: maybe (QueuedMail, Option<QueueSideEffects>) is worth an alias?

@hpk42 hpk42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall i think the PR is larger than it needs to be.

Conceptually there is one main issue IMO: For BCC-self-copies this PR re-resolves current relay list, but not for the recipient addresses. In bad network/partially-offline situations messages can be in the queue for long enough that a keyupdate arrived. Logically, recipient fingerprints should be derived to addresses at sending time, and not fixated at queuing time. At send-time we anyway read the public keys, so it wouldn't cause more IO. Messages then either have fingerprints as recipients, or addresses but never both at the same time. After sending to addresses in a chunk succeeded, they could be written to a new sent_finished column, so that the next address-derivation can skip them.

Comment thread src/mimefactory.rs
// eg. are just freetext and/or do not follow any standard.
headers.push((
"Chat-Disposition-Notification-To",
mail_builder::headers::raw::Raw::new(self.from_addr.clone()).into(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would need to be re-rendered on transport change as well but i instead suggest to relax the check in https://github.com/chatmail/core/blob/link2xt/late-encryption/src/mimeparser.rs#L955 and treat the header existence as a boolean, or compare to all relay addresses.
There also is another use of self.from_addr for the Sender header but i think it's not critical on the receiver side. Ideally there would be no self.from_addr left at all.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opened #8681 now.
For Chat-Disposition-Notification-To and Sender, i have edited them in the first message #8572 while doing #8645 which is also split out of this PR in attempt to get rid of from_addr.

Sender is essentially a boolean flag saying "this is a bridge bot, show display name with ~ and save it in the message".

Comment thread src/sql/migrations.rs
migration_version,
)
.await?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doesn't this orphan currently queued messages in the existing smtp table? At least they should be marked failed i think.

Comment thread src/smtp.rs Outdated
Comment thread src/smtp.rs
};
let mut recipients = queued_mail.recipients.clone();
if queued_mail.bcc_self {
add_self_recipients(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this adds the self-recipients on every retry without dedup. After a partially sent chunked message, the remainder written back to the db further down already contains them.

Comment thread src/smtp.rs Outdated
Comment thread docs/schema.sql
Comment on lines +819 to +827
-- Replaced with smtp2.
CREATE TABLE smtp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT NOT NULL, -- Message-ID
mime TEXT NOT NULL, -- SMTP payload
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
recipients TEXT NOT NULL, -- List of recipients separated by space
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why move this table and increase the diff?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

All unused tables are at the end currently, below the -- Deprecated and unused tables. comment.

Comment thread src/test_utils.rs
@@ -681,33 +716,70 @@ ORDER BY id"
}

pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec<SentMessage<'a>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please use a helper instead of the duplications in pop_sent_msg_ext, here and first_row_in_smtp_queue.

Comment thread src/mimefactory.rs
}

#[derive(Debug, Clone)]
pub(crate) enum QueuedEncryption {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this new enum is much "better" as you say in the PR description. It's only about dropping addresses but e.g. keyupdates just used empty strings on main and render_queued_mail dropped the address unconditionally. So overall, it's more diff and more LOCs without much gain IMO.

Comment thread src/chat.rs
Comment on lines -3024 to -3025
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why did the transaction have to go? Removing it here seems not neccessary, and produces follow-up Todos.

Comment thread src/keyupdate.rs
let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
let keys = chunk.iter().map(|r| r.public_key.clone()).collect();
let rendered_message = render_keyupdate_message(context, &rfc724_mid, keys).await?;
insert_into_smtp(context, &rfc724_mid, &envelope, rendered_message).await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not let insert_into_smtp take a QueuedMail instead of the rendered string? Removing the helper and inlining it three times in different ways seems unnecessary.

It was incorrectly converting unused row ID to MsgId type
and selecting already known msg_id.
@link2xt
link2xt force-pushed the link2xt/late-encryption branch from 7eb324f to 84dd1f4 Compare September 11, 2026 02:51
This is similar to mimefactory::Encryption,
but does not have email addresses for asymmetrically encrypted messages.
Queued messages don't need email addresses for public keys.
Addresses are only needed to render Autocrypt-Gossip headers.
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.
@link2xt
link2xt force-pushed the link2xt/late-encryption branch from 84dd1f4 to 8fded29 Compare September 11, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants