mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
* [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>
125 lines
3.6 KiB
C++
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
|