From 526d528e96a558b2e9e77085d3d43721c400c4ad Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:16:33 +0100 Subject: [PATCH] [Server] Serve the e-mail queue through the database interface (#7858) --- .../server/remote/server_database_interface.h | 36 +++++++++ servatrice/src/servatrice.cpp | 54 ++++--------- .../src/servatrice_database_interface.cpp | 77 +++++++++++++++++++ .../src/servatrice_database_interface.h | 4 + 4 files changed, 131 insertions(+), 40 deletions(-) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h index 4d4f42de0..cd8e1ef74 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -780,6 +780,42 @@ public: return {}; } + /** @brief One queued activation e-mail, with the address exactly as stored. */ + struct PendingActivationMailRecord + { + QString userName; + QString emailAddress; + QString token; + }; + /** @brief One queued password-reset e-mail, identified by its queue row. */ + struct PendingPasswordResetRecord + { + qint64 rowId = 0; + QString userName; + QString emailAddress; + QString token; + }; + /** @brief Fills the queued activation e-mails; false when the query fails. */ + virtual bool getPendingActivationMails(QList & /* mails */) + { + return false; + } + /** @brief Fills the unsent password-reset e-mails; false when the query fails. */ + virtual bool getPendingPasswordResets(QList & /* resets */) + { + return false; + } + /** @brief Deletes a delivered activation e-mail queue row; false when the statement fails. */ + virtual bool deleteActivationEmail(const QString & /* userName */) + { + return false; + } + /** @brief Marks a password-reset queue row as emailed, by row id when known; false on failure. */ + virtual bool markPasswordResetEmailDelivered(const QString & /* userName */, qint64 /* rowId */) + { + return false; + } + virtual void clearSessionTables() { } diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 30d2619ad..b6b4a8e61 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -50,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -870,10 +869,8 @@ void Servatrice::statusUpdate() if (getRegistrationEnabled() && getEnableInternalSMTPClient()) { if (getRequireEmailActivationEnabled()) { - auto servDbSelQuery = servatriceDatabaseInterface->prepareQuery("select a.name, b.email, b.token from " - "{prefix}_activation_emails a left join " - "{prefix}_users b on a.name = b.name"); - if (!servatriceDatabaseInterface->execSqlQuery(servDbSelQuery)) { + QList activationMails; + if (!servatriceDatabaseInterface->getPendingActivationMails(activationMails)) { return; } @@ -881,32 +878,23 @@ void Servatrice::statusUpdate() // activation e-mail is the database row, which is only removed once delivery has been // confirmed by the SMTP client (see onEmailDelivered). This makes sending resilient to // transient SMTP failures and server restarts. - while (servDbSelQuery->next()) { - const QString userName = servDbSelQuery->value(0).toString(); - const auto emailAddress = EmailParser::getParsedEmailAddress(servDbSelQuery->value(1).toString()); - const QString token = servDbSelQuery->value(2).toString(); - - smtpClient->enqueueActivationTokenMail(userName, emailAddress, token); + for (const auto &mail : activationMails) { + smtpClient->enqueueActivationTokenMail( + mail.userName, EmailParser::getParsedEmailAddress(mail.emailAddress), mail.token); } } if (getEnableForgotPassword()) { - auto *forgotPwQuery = servatriceDatabaseInterface->prepareQuery( - "select a.id, a.name, b.email, b.token from {prefix}_forgot_password a left join {prefix}_users b on " - "a.name = b.name where a.emailed = 0"); - if (!servatriceDatabaseInterface->execSqlQuery(forgotPwQuery)) { + QList passwordResets; + if (!servatriceDatabaseInterface->getPendingPasswordResets(passwordResets)) { return; } // Rows are intentionally left with emailed = 0 here; they are marked emailed only once // delivery is confirmed by the SMTP client (see onEmailDelivered). - while (forgotPwQuery->next()) { - const qint64 rowId = forgotPwQuery->value(0).toLongLong(); - const QString userName = forgotPwQuery->value(1).toString(); - const auto emailAddress = EmailParser::getParsedEmailAddress(forgotPwQuery->value(2).toString()); - const QString token = forgotPwQuery->value(3).toString(); - - smtpClient->enqueueForgotPasswordTokenMail(userName, emailAddress, token, rowId); + for (const auto &reset : passwordResets) { + smtpClient->enqueueForgotPasswordTokenMail( + reset.userName, EmailParser::getParsedEmailAddress(reset.emailAddress), reset.token, reset.rowId); } } @@ -952,27 +940,13 @@ bool Servatrice::cleanupDatabaseForEmail(const QString &userName, EmailType type { Q_UNUSED(token); switch (type) { - case EmailType::Activation: { - QSqlQuery *query = - servatriceDatabaseInterface->prepareQuery("delete from {prefix}_activation_emails where name = :name"); - query->bindValue(":name", userName); - return servatriceDatabaseInterface->execSqlQuery(query); - } - case EmailType::ForgotPassword: { + case EmailType::Activation: + return servatriceDatabaseInterface->deleteActivationEmail(userName); + case EmailType::ForgotPassword: // Scope the update to the row that was actually sent: the table // holds one row per request, and marking by name would also mark // rows for newer tokens that were never delivered. - if (rowId >= 0) { - QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( - "update {prefix}_forgot_password set emailed = 1 where id = :id"); - query->bindValue(":id", rowId); - return servatriceDatabaseInterface->execSqlQuery(query); - } - QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( - "update {prefix}_forgot_password set emailed = 1 where name = :name"); - query->bindValue(":name", userName); - return servatriceDatabaseInterface->execSqlQuery(query); - } + return servatriceDatabaseInterface->markPasswordResetEmailDelivered(userName, rowId); } return false; } diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 50c51cfdd..dd59fba98 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -3421,6 +3421,83 @@ QList Servatrice_DatabaseInterfac return rooms; } +bool Servatrice_DatabaseInterface::getPendingActivationMails(QList &mails) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("select a.name, b.email, b.token from " + "{prefix}_activation_emails a left join " + "{prefix}_users b on a.name = b.name"); + if (!execSqlQuery(query)) { + return false; + } + + while (query->next()) { + PendingActivationMailRecord mail; + mail.userName = query->value(0).toString(); + mail.emailAddress = query->value(1).toString(); + mail.token = query->value(2).toString(); + mails.append(mail); + } + + return true; +} + +bool Servatrice_DatabaseInterface::getPendingPasswordResets(QList &resets) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery( + "select a.id, a.name, b.email, b.token from {prefix}_forgot_password a left join {prefix}_users b on " + "a.name = b.name where a.emailed = 0"); + if (!execSqlQuery(query)) { + return false; + } + + while (query->next()) { + PendingPasswordResetRecord reset; + reset.rowId = query->value(0).toLongLong(); + reset.userName = query->value(1).toString(); + reset.emailAddress = query->value(2).toString(); + reset.token = query->value(3).toString(); + resets.append(reset); + } + + return true; +} + +bool Servatrice_DatabaseInterface::deleteActivationEmail(const QString &userName) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("delete from {prefix}_activation_emails where name = :name"); + query->bindValue(":name", userName); + return execSqlQuery(query); +} + +bool Servatrice_DatabaseInterface::markPasswordResetEmailDelivered(const QString &userName, qint64 rowId) +{ + if (!checkSql()) { + return false; + } + + if (rowId >= 0) { + QSqlQuery *query = prepareQuery("update {prefix}_forgot_password set emailed = 1 where id = :id"); + query->bindValue(":id", rowId); + return execSqlQuery(query); + } + + QSqlQuery *query = prepareQuery("update {prefix}_forgot_password set emailed = 1 where name = :name"); + query->bindValue(":name", userName); + return execSqlQuery(query); +} + bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName) { if (!checkSql()) { diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 38a8e7e9f..18f7f67a2 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -282,6 +282,10 @@ public: QList getServerListEntries() override; void recordUptime(const UptimeRecord &record) override; QList getBootstrapRooms(int serverId) override; + bool getPendingActivationMails(QList &mails) override; + bool getPendingPasswordResets(QList &resets) override; + bool deleteActivationEmail(const QString &userName) override; + bool markPasswordResetEmailDelivered(const QString &userName, qint64 rowId) override; bool removeUserAvatar(const QString &userName) override; // Outcome of removeUserBanner(), so the caller can tell a missing user apart // from a user who simply has no banner card set.