mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 11:33:25 -08:00
[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:
parent
1a4d89ff8f
commit
8968709642
7 changed files with 655 additions and 133 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue