654 lines
20 KiB
PHP
654 lines
20 KiB
PHP
<?php
|
|
|
|
namespace php;
|
|
|
|
use Exception;
|
|
use Monolog\Logger;
|
|
use PDO;
|
|
use PHPMailer\PHPMailer\Exception as MailerException;
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\SMTP;
|
|
|
|
|
|
/**
|
|
* Queues up mails and sends them when appropriate.
|
|
*/
|
|
class Mailer
|
|
{
|
|
/**
|
|
* @var Logger The logger to use for logging.
|
|
*/
|
|
private Logger $logger;
|
|
/**
|
|
* @var array<string, array<string, mixed>> The configuration to use for mailing.
|
|
*/
|
|
private array $config;
|
|
/**
|
|
* @var PDO The database connection to interact with.
|
|
*/
|
|
private PDO $conn;
|
|
|
|
|
|
/**
|
|
* Constructs a new mailer.
|
|
*
|
|
* @param array<string, array<string, mixed>> $config the configuration to use for mailing
|
|
* @param Logger $logger the logger to use for logging
|
|
* @param PDO $conn the connection to the email database
|
|
*/
|
|
public function __construct(array $config, Logger $logger, PDO $conn)
|
|
{
|
|
$this->logger = $logger;
|
|
$this->config = $config;
|
|
$this->conn = $conn;
|
|
}
|
|
|
|
|
|
/**
|
|
* Populates the database with the necessary structures for emails.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function install(): void
|
|
{
|
|
$this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL,
|
|
recipient TEXT NOT NULL,
|
|
arg1 TEXT NOT NULL DEFAULT(''),
|
|
arg2 TEXT NOT NULL DEFAULT(''),
|
|
PRIMARY KEY (type, recipient, arg1, arg2));");
|
|
}
|
|
|
|
|
|
/**
|
|
* Queues an email to be sent.
|
|
*
|
|
* @param Email $email the email to queue
|
|
* @return Response a satisfied `Response` if the email was queued, or an unsatisfied `Response` otherwise
|
|
*/
|
|
public function queue_email(Email $email): Response
|
|
{
|
|
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2)
|
|
VALUES (:type, :recipient, :arg1, :arg2);");
|
|
$stmt->bindValue(":type", $email->type);
|
|
$stmt->bindValue(":recipient", $email->recipient);
|
|
$stmt->bindValue(":arg1", $email->arg1);
|
|
$stmt->bindValue(":arg2", $email->arg2);
|
|
|
|
return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null);
|
|
}
|
|
|
|
/**
|
|
* Sends all emails in the queue.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function process_queue(): void
|
|
{
|
|
// Open mailer
|
|
$mailer = new PHPMailer();
|
|
$mailer->IsSMTP();
|
|
$mailer->CharSet = "UTF-8";
|
|
|
|
$mailer->SMTPAuth = true;
|
|
$mailer->SMTPDebug = SMTP::DEBUG_OFF;
|
|
$mailer->SMTPKeepAlive = true;
|
|
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
|
$mailer->Host = $this->config["mail"]["host"];
|
|
$mailer->Port = $this->config["mail"]["port"];
|
|
$mailer->Username = $this->config["mail"]["username"];
|
|
$mailer->Password = $this->config["mail"]["password"];
|
|
try {
|
|
$mailer->setFrom($this->config["mail"]["username"], $this->config["mail"]["from_name"]);
|
|
} catch (MailerException $exception) {
|
|
$this->logger->error("Failed to set 'from' address while processing queue.", ["cause" => $exception]);
|
|
$mailer->smtpClose();
|
|
}
|
|
|
|
// Get queue
|
|
$stmt = $this->conn->prepare("SELECT type, recipient, arg1, arg2 FROM email_tasks;");
|
|
$stmt->execute();
|
|
$email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Process queue
|
|
$stmt = $this->conn->prepare("DELETE FROM email_tasks
|
|
WHERE type=:type AND recipient=:recipient AND arg1=:arg1 AND arg2=:arg2;");
|
|
$stmt->bindParam(":type", $type);
|
|
$stmt->bindParam(":recipient", $recipient);
|
|
$stmt->bindParam(":arg1", $arg1);
|
|
$stmt->bindParam(":arg2", $arg2);
|
|
foreach ($email_tasks as ["type" => $type, "recipient" => $recipient, "arg1" => $arg1, "arg2" => $arg2]) {
|
|
try {
|
|
$email = Email::deserialize($type, $recipient, $arg1, $arg2);
|
|
$mailer->Subject = $email->getSubject();
|
|
$mailer->Body = $email->getBody($this->config);
|
|
|
|
try {
|
|
$mailer->addAddress($recipient);
|
|
$mailer->send();
|
|
} catch (MailerException $exception) {
|
|
$this->logger->error(
|
|
"Failed to send mail.",
|
|
["cause" => $exception, "email" => $email]
|
|
);
|
|
$mailer->getSMTPInstance()->reset();
|
|
}
|
|
|
|
$mailer->clearAddresses();
|
|
} catch (Exception $exception) {
|
|
$this->logger->error(
|
|
"Failed to send mail.",
|
|
["cause" => $exception]
|
|
);
|
|
$mailer->getSMTPInstance()->reset();
|
|
}
|
|
|
|
$stmt->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A serializable email that can be queued in a database and can be sent.
|
|
*
|
|
* When serialized, an email is represented by the type of email and two arguments. When deserialized, an email can be
|
|
* constructed from those arguments, and the implementation returns a specific subject and message.
|
|
*/
|
|
abstract class Email
|
|
{
|
|
/**
|
|
* @var string A string identifying the type of email.
|
|
*/
|
|
public string $type;
|
|
/**
|
|
* @var string The intended recipient of the email.
|
|
*/
|
|
public string $recipient;
|
|
/**
|
|
* @var string The first argument to construct the email.
|
|
*/
|
|
public string $arg1 = "";
|
|
/**
|
|
* @var string The second argument to construct the email.
|
|
*/
|
|
public string $arg2 = "";
|
|
|
|
|
|
/**
|
|
* Returns the subject header of the email.
|
|
*
|
|
* @return string the subject header of the email
|
|
*/
|
|
public abstract function getSubject(): string;
|
|
|
|
/**
|
|
* Returns the body of the email.
|
|
*
|
|
* @param array<string, array<string, mixed>> $config the software configuration
|
|
* @return string the body of the email
|
|
*/
|
|
public abstract function getBody(array $config): string;
|
|
|
|
|
|
/**
|
|
* Deserializes an email back into an instance.
|
|
*
|
|
* Only types of emails that are known can be deserialized.
|
|
*
|
|
* @param string $type the type of email to deserialize
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $arg1 the first argument to construct the email
|
|
* @param string $arg2 the second argument to construct the email
|
|
* @return Email a deserialized email
|
|
* @throws Exception if the `type` is not recognized
|
|
*/
|
|
public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email
|
|
{
|
|
return match ($type) {
|
|
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
|
|
VerifyEmailEmail::TYPE => new VerifyEmailEmail($recipient, $arg1),
|
|
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
|
|
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
|
|
ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1),
|
|
NotifyArticleDeletedEmail::TYPE => new NotifyArticleDeletedEmail($recipient, $arg1),
|
|
NotifyArticleUndeletedEmail::TYPE => new NotifyArticleUndeletedEmail($recipient, $arg1),
|
|
NotifyStatusChangedEmail::TYPE => new NotifyStatusChangedEmail($recipient, $arg1, $arg2),
|
|
default => throw new Exception("Unknown email type $type."),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to be sent to a recently registered user, including instructions for email verification.
|
|
*/
|
|
class RegisterEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "register";
|
|
|
|
/**
|
|
* @var string The token to verify the email address with.
|
|
*/
|
|
public string $token;
|
|
|
|
|
|
/**
|
|
* Constructs an email to be sent to a recently registered user, including instructions for email verification.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $token the token to verify the email address with
|
|
*/
|
|
public function __construct(string $recipient, string $token)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->token = $token;
|
|
$this->arg1 = $token;
|
|
}
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "Welcome to Death Notifier";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
|
|
|
return
|
|
"You have successfully created an account with Death Notifier. " .
|
|
"Welcome!" .
|
|
"\n\n" .
|
|
"Until you verify your email address, you will not receive any notifications. " .
|
|
"You can verify your email address by clicking the link below. " .
|
|
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
|
|
"\n" .
|
|
"Verify: $verify_path" .
|
|
"\n\n" .
|
|
"If you did not create this account, you can delete the account. " .
|
|
"Go to the Death Notifier website, reset your password, log in, and delete the account." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to help a user verify their email address.
|
|
*/
|
|
class VerifyEmailEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "verify-email";
|
|
|
|
/**
|
|
* @var string The token to verify the email address with.
|
|
*/
|
|
public string $token;
|
|
|
|
|
|
/**
|
|
* Constructs an email to help a user verify their email address.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $token the token to verify the email address with
|
|
*/
|
|
public function __construct(string $recipient, string $token)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->token = $token;
|
|
$this->arg1 = $token;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "Verify your email address";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
|
|
|
return
|
|
"You requested a new verification link for your Death Notifier account. " .
|
|
"You can verify your email address by clicking the link below. " .
|
|
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes. " .
|
|
"Until you verify your email address, you will not receive any notifications." .
|
|
"\n" .
|
|
"Verify: $verify_path" .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email informing a user that their email has been changed, and needs verification.
|
|
*/
|
|
class ChangedEmailEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "changed-email";
|
|
|
|
/**
|
|
* @var string The token to verify the email address with.
|
|
*/
|
|
public string $token;
|
|
|
|
|
|
/**
|
|
* Constructs an email informing a user that their email has been changed, and needs verification.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $token the token to verify the email address with
|
|
*/
|
|
public function __construct(string $recipient, string $token)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->token = $token;
|
|
$this->arg1 = $token;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "Verify your new email address";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
|
|
|
return
|
|
"You changed the email address of your Death Notifier account. " .
|
|
"Until you verify your email address, you will not receive any notifications. " .
|
|
"You can verify your new email address by clicking the link below. " .
|
|
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
|
|
"\n" .
|
|
"Verify: $verify_path" .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email informing a user that their password has been changed.
|
|
*/
|
|
class ChangedPasswordEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "changed-password";
|
|
|
|
|
|
/**
|
|
* Constructs an email informing a user that their email has been changed, and needs verification.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
*/
|
|
public function __construct(string $recipient)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "Your password has been changed";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
|
|
return
|
|
"You changed the password of your Death Notifier account." .
|
|
"\n\n" .
|
|
"If you did not change the password of your account, go to the Death Notifier website and use the forgot " .
|
|
"password option to change your password back." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to help a user reset their password.
|
|
*/
|
|
class ResetPasswordEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "reset-password";
|
|
|
|
/**
|
|
* @var string The token to reset the password with.
|
|
*/
|
|
public string $token;
|
|
|
|
|
|
/**
|
|
* Constructs an email to help a user reset their password.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $token the token to reset the password with
|
|
*/
|
|
public function __construct(string $recipient, string $token)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->token = $token;
|
|
$this->arg1 = $token;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "Reset your password";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
$verify_path =
|
|
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
|
|
|
return
|
|
"You requested a password reset link for your Death Notifier account. " .
|
|
"You can choose a new password by clicking the link below. " .
|
|
"This link expires after " . UserManager::MINUTES_VALID_PASSWORD_RESET . " minutes." .
|
|
"\n" .
|
|
"Reset password: $verify_path" .
|
|
"\n\n" .
|
|
"If you did not request a new password, you can safely ignore this message." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to inform a user that a tracked article has been deleted.
|
|
*/
|
|
class NotifyArticleDeletedEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "notify-article-deleted";
|
|
|
|
/**
|
|
* @var string The name of the article that was deleted.
|
|
*/
|
|
public string $name;
|
|
|
|
|
|
/**
|
|
* Constructs an email to inform a user that a tracked article has been deleted.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $name the name of the article that was deleted
|
|
*/
|
|
public function __construct(string $recipient, string $name)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->name = $name;
|
|
$this->arg1 = $name;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "$this->name article has been deleted";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
|
|
return
|
|
"The Wikipedia article about $this->name has been deleted. " .
|
|
"Death Notifier is now unable to send you a notification if $this->name dies. " .
|
|
"If the Wikipedia article is ever re-created, Death Notifier will automatically resume tracking this " .
|
|
"article, and you will receive another notification." .
|
|
"\n\n" .
|
|
"You are receiving this message because of the preferences in your Death Notifier account. " .
|
|
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
|
|
"preferences." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to inform a user that a tracked article has been re-created.
|
|
*/
|
|
class NotifyArticleUndeletedEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "notify-article-undeleted";
|
|
|
|
/**
|
|
* @var string The name of the article that was re-created.
|
|
*/
|
|
public string $name;
|
|
|
|
|
|
/**
|
|
* Constructs an email to inform a user that a tracked article has been re-created.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $name the name of the article that was re-created
|
|
*/
|
|
public function __construct(string $recipient, string $name)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->name = $name;
|
|
$this->arg1 = $name;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "$this->name article has been re-created";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
|
|
return
|
|
"The Wikipedia article about $this->name has been re-created. " .
|
|
"Death Notifier will once again track the article and notify you if $this->name dies." .
|
|
"\n\n" .
|
|
"You are receiving this message because of the preferences in your Death Notifier account. " .
|
|
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
|
|
"preferences." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An email to inform a user a tracker person's status has changed.
|
|
*/
|
|
class NotifyStatusChangedEmail extends Email
|
|
{
|
|
/**
|
|
* A string identifying the type of email.
|
|
*/
|
|
public const TYPE = "notify-status-changed";
|
|
|
|
/**
|
|
* @var string The name of the person whose status has changed.
|
|
*/
|
|
public string $name;
|
|
/**
|
|
* @var string The new status of the person.
|
|
*/
|
|
public string $new_status;
|
|
|
|
|
|
/**
|
|
* Constructs an email to inform a user someone has died.
|
|
*
|
|
* @param string $recipient the intended recipient of the email
|
|
* @param string $name the name of the person who died
|
|
* @param string $new_status the new status of the person
|
|
*/
|
|
public function __construct(string $recipient, string $name, string $new_status)
|
|
{
|
|
$this->type = self::TYPE;
|
|
$this->recipient = $recipient;
|
|
|
|
$this->name = $name;
|
|
$this->arg1 = $name;
|
|
|
|
$this->new_status = $new_status;
|
|
$this->arg2 = $new_status;
|
|
}
|
|
|
|
|
|
public function getSubject(): string
|
|
{
|
|
return "$this->name may be $this->new_status";
|
|
}
|
|
|
|
public function getBody(array $config): string
|
|
{
|
|
$base_path = $config["server"]["base_path"];
|
|
|
|
return
|
|
"Someone has edited Wikipedia to state that $this->name is $this->new_status. " .
|
|
"For more information, read their Wikipedia article at " .
|
|
"https://en.wikipedia.org/wiki/" . rawurlencode($this->name) .
|
|
"\n\n" .
|
|
"You are receiving this message because of the preferences in your Death Notifier account. " .
|
|
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
|
|
"preferences." .
|
|
"\n\n" .
|
|
$base_path;
|
|
}
|
|
}
|