[Server] Serve the e-mail queue through the database interface (#7858)

This commit is contained in:
BruebachL 2026-11-10 12:16:33 +01:00 • committed by GitHub
parent 9087d0d3fe
commit 526d528e96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 131 additions and 40 deletions

View file

@ -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<PendingActivationMailRecord> & /* mails */)
{
return false;
}
/** @brief Fills the unsent password-reset e-mails; false when the query fails. */
virtual bool getPendingPasswordResets(QList<PendingPasswordResetRecord> & /* 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()
{
}

View file

@ -50,7 +50,6 @@
#include <QSet>
#include <QSharedPointer>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSsl>
#include <QString>
#include <QStringList>
@ -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<Server_DatabaseInterface::PendingActivationMailRecord> 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<Server_DatabaseInterface::PendingPasswordResetRecord> 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;
}

View file

@ -3421,6 +3421,83 @@ QList<Server_DatabaseInterface::BootstrapRoomRecord> Servatrice_DatabaseInterfac
return rooms;
}
bool Servatrice_DatabaseInterface::getPendingActivationMails(QList<PendingActivationMailRecord> &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<PendingPasswordResetRecord> &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()) {

View file

@ -282,6 +282,10 @@ public:
QList<ServerListEntryRecord> getServerListEntries() override;
void recordUptime(const UptimeRecord &record) override;
QList<BootstrapRoomRecord> getBootstrapRooms(int serverId) override;
bool getPendingActivationMails(QList<PendingActivationMailRecord> &mails) override;
bool getPendingPasswordResets(QList<PendingPasswordResetRecord> &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.