[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

@ -1,26 +1,67 @@
#ifndef SMTPCLIENT_H
#define SMTPCLIENT_H
#include "smtp/qxtmailmessage.h"
#include <QDateTime>
#include <QHash>
#include <QObject>
#include <QString>
#include <QTimer>
#include <qtmetamacros.h>
class QxtSmtp;
class QByteArray;
/**
* @brief Types of e-mail the SMTP client is able to deliver.
*/
enum class EmailType
{
Activation,
ForgotPassword
};
Q_DECLARE_METATYPE(EmailType)
/**
* @brief Why a permanent e-mail delivery failure occurred.
*/
enum class FailureReason
{
RetryExhausted,
RecipientRejected,
SenderRejected
};
Q_DECLARE_METATYPE(FailureReason)
class SmtpClient : public QObject
{
Q_OBJECT
public:
SmtpClient(QObject *parent = 0);
~SmtpClient();
SmtpClient(QObject *parent = nullptr);
~SmtpClient() override;
public slots:
bool enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token);
bool enqueueForgotPasswordTokenMail(const QString &nickname,
const QString &recipient,
const QString &token,
qint64 rowId = -1);
void sendAllEmails();
signals:
void mailDelivered(const QString &userName, EmailType type, const QString &token, qint64 rowId);
void mailPermanentlyFailed(const QString &userName,
EmailType type,
FailureReason reason,
const QString &token,
qint64 rowId);
protected:
QxtSmtp *smtp;
public slots:
bool enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token);
bool enqueueForgotPasswordTokenMail(const QString &nickname, const QString &recipient, const QString &token);
void sendAllEmails();
protected slots:
void authenticated();
void authenticationFailed(const QByteArray &msg);
@ -30,10 +71,55 @@ protected slots:
void encrypted();
void encryptionFailed(const QByteArray &msg);
void finished();
void idleTimeout();
void mailFailed(int mailID, int errorCode, const QByteArray &msg);
void mailSent(int mailID);
void recipientRejected(int mailID, const QString &address, const QByteArray &msg);
void senderRejected(int mailID, const QString &address, const QByteArray &msg);
private:
struct PendingEmail
{
QString userName;
EmailType type;
QString token;
// Database row id for tables that allow multiple rows per user
// ({prefix}_forgot_password); -1 when the name alone identifies the row.
qint64 rowId = -1;
QxtMailMessage message;
int attempts = 0;
int maxAttempts = 0;
int retryDelay = 0;
QDateTime nextAttempt;
};
PendingEmail buildEmail(const QString &nickname,
EmailType type,
const QString &subject,
const QString &body,
const QString &recipient,
const QString &token,
const QString &email,
qint64 rowId = -1);
bool enqueue(const PendingEmail &email);
void relayoutForSending();
QDateTime nextRetryTime(int attempts, int retryDelay) const;
void recordFailed(int mailID);
void recordConnectionFailure();
bool validateEmailFields(const QString &email,
const QString &subject,
const QString &body,
const QString &recipient,
const QString &token,
const QString &nickname);
int maxRetries() const;
int retryDelaySeconds() const;
QHash<QString, PendingEmail> pendingEmails;
QHash<int, QString> mailIdToUser;
QTimer *idleTimer;
int connectionAttempts = 0;
QDateTime connectionBackoffUntil;
};
#endif
#endif