Cockatrice/servatrice/src/smtpclient.h
BruebachL 8968709642
[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>
2026-10-11 20:59:32 +02:00

125 lines
3.6 KiB
C++

#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 = 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;
protected slots:
void authenticated();
void authenticationFailed(const QByteArray &msg);
void connected();
void connectionFailed(const QByteArray &msg);
void disconnected();
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