[SMTP] Add reliable email delivery with retry and database-backed queue (#7240)

* [SMTP] Add reliable email delivery with retry and database-backed queue

Rework the SMTP client from fire-and-forget sending into a persistent
queue with exponential backoff and per-message retry budget, backed by
the existing database rows as the durable source of truth.

- Keep pending emails in an in-memory queue with attempts/backoff
  tracking; enqueue without deleting database rows, and only remove the
  row once delivery is confirmed via the mailDelivered signal.
- Add exponential backoff (doubling, capped at 30 min) between delivery
  attempts, bounded by new smtp/maxretries and smtp/retrydelay settings.
- Track connection-layer failures (connect/auth/encrypt) with their own
  backoff; route failures to recordFailed/recordConnectionFailure so
  undeliverable mail eventually drops from the queue instead of
  retrying forever.
- Treat rejected recipients/senders as permanent failures.
- Rebuild the SMTP buffer from the authoritative queue before each send
  via a new QxtSmtp::reset() so reconnects never duplicate deliveries.
- Start the idle timeout after authentication, not on TCP connect, so
  slow SSL/STARTTLS handshakes are not cut short.

* [SMTP] Fix retry accounting, backoff reset, and token dedupe

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-11 20:59:32 +02:00 • committed by GitHub
parent 1a4d89ff8f
commit 8968709642
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 655 additions and 133 deletions

View file

@ -94,6 +94,38 @@ int QxtSmtp::pendingMessages() const
return qxt_d().pending.count();
}
/*!
* \brief Drops all queued (not yet transmitted) messages without sending them.
*
* Any message still awaiting delivery is discarded. This is used by callers that
* keep their own authoritative queue and wish to rebuild the SMTP buffer from scratch
* (e.g. after a reconnect) without risking duplicates.
*/
void QxtSmtp::clearPending()
{
qxt_d().pending.clear();
}
/*!
* \brief Clears the outgoing queue and resets the SMTP state machine.
*
* Unlike clearPending(), this also resets the internal state to Disconnected so
* that subsequent send() calls will not fire sendNext() prematurely on a
* not-yet-connected socket. Use this when rebuilding the queue from scratch
* before a fresh connectToHost() / connectToSecureHost() cycle.
*/
void QxtSmtp::reset()
{
qxt_d().pending.clear();
qxt_d().extensions.clear();
qxt_d().buffer.clear();
qxt_d().recipients.clear();
qxt_d().rcptNumber = 0;
qxt_d().rcptAck = 0;
qxt_d().mailAck = false;
qxt_d().state = QxtSmtpPrivate::Disconnected;
}
QTcpSocket *QxtSmtp::socket() const
{
return qxt_d().socket;