death-notifier/src/main/php/Mailer.php

270 lines
9.9 KiB
PHP

<?php
namespace php;
use Monolog\Logger;
use PDO;
use PHPMailer\PHPMailer\Exception;
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 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 Logger $logger the logger to use for logging
* @param array $config the configuration to use for mailing
*/
public function __construct(Logger $logger, array $config, 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,
arg1 text default null,
arg2 text default null,
PRIMARY KEY (type, arg1, arg2));");
}
/**
* Queues an email to be sent for a newly registered user.
*
* @param string $email the email address to send the email to
* @param string $token the token the user can verify their email address with
* @return Response an empty satisfied response
*/
public function queue_registration(string $email, string $token): Response
{
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, arg1, arg2)
VALUES ('register', :email, :token);");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":token", $token);
$stmt->execute();
return new Response(payload: null, satisfied: true);
}
/**
* Creates the email subject and body to be sent to a newly registered user.
*
* @param string $email the email address of the newly registered user
* @param string $token the email address verification token of the newly registered user
* @return string[] the subject and body of the email
*/
private function create_register_email(string $email, string $token): array
{
$verify_path = $this->config["server"]["base_path"] .
"?action=verify-email" .
"&email=" . rawurlencode($email) .
"&token=$token";
// TODO: Nicer message!
// TODO: What if user did not create account themselves?
return [
"Created account for Death Notifier",
"An account has been created for $email for Death Notifier. " .
"Until you verify your email address, you will not receive any notifications. " .
"You can verify your email address by clicking the link below." .
"\n\n" .
"Verify: $verify_path"
];
}
/**
* Queues an email to be sent to a user who should verify their email address.
*
* @param string $email the email address to send the email to
* @param string $token the token the user can verify their email address with
* @return Response an empty satisfied response
*/
public function queue_verification(string $email, string $token): Response
{
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, arg1, arg2)
VALUES ('verify', :email, :token);");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":token", $token);
$stmt->execute();
return new Response(payload: null, satisfied: true);
}
/**
* Creates the email subject and body to be sent to a user who should verify their email address.
*
* @param string $email the email address of the user
* @param string $token the email address verification token of the user
* @return string[] the subject and body of the email
*/
private function create_verify_email(string $email, string $token): array
{
$base_path = $this->config["server"]["base_path"];
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($email) . "&token=$token";
// TODO: What if user did not change email address?
// TODO: Separate "verify after changing email" and "resending verify email"
return [
"Verify your email address",
"Your email address for Death Notifier has not been verified yet. " .
"Until you verify your email address, you will not receive any notifications. " .
"You can verify your email address by clicking the link below." .
"\n\n" .
"Verify: $verify_path"
];
}
/**
* Queues an email to be sent to a user to notify them of a tracked person's death.
*
* @param string $email the email address to send the death notification to
* @param string $name the name of the person who probably passed away
* @return void
*/
public function queue_death_notification(string $email, string $name): void
{
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, arg1, arg2)
VALUES ('notify-death', :email, :name);");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":name", $name);
$stmt->execute();
}
/**
* Creates the email subject and body to be sent to notify a user of a tracked person's death.
*
* @param string $name the name of the person who has likely passed away
* @return string[] the subject and body of the email
*/
private function create_death_notification_email(string $name): array
{
// TODO: Add unsubscribe link
return [
"$name may have passed away",
"Someone has edited the Wikipedia page of $name to state that they have passed away. " .
"For more information, read their Wikipedia page at " .
"https://en.wikipedia.org/wiki/" . rawurlencode($name)
];
}
/**
* 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 (Exception $exception) {
$this->logger->error("Failed to set 'from' address while processing queue.", ["cause" => $exception]);
$mailer->smtpClose();
}
// Get queue
$stmt = $this->conn->prepare("SELECT type, 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 arg1=:arg1 AND arg2=:arg2;");
$stmt->bindParam(":type", $type);
$stmt->bindParam(":arg1", $arg1);
$stmt->bindParam(":arg2", $arg2);
foreach ($email_tasks as ["type" => $type, "arg1" => $arg1, "arg2" => $arg2]) {
// TODO: Reduce duplication between branches
switch ($type) {
case "register":
[$mailer->Subject, $mailer->Body] = $this->create_register_email($arg1, $arg2);
try {
$mailer->addAddress($arg1);
$mailer->send();
$stmt->execute();
} catch (Exception $exception) {
$this->logger->error(
"Failed to send register mail.",
["cause" => $exception, "recipient" => $arg1]
);
$mailer->getSMTPInstance()->reset();
}
break;
case "verify":
[$mailer->Subject, $mailer->Body] = $this->create_verify_email($arg1, $arg2);
try {
// TODO: Also send to old email address (by storing old email in `users` table?)
$mailer->addAddress($arg1);
$mailer->send();
$stmt->execute();
} catch (Exception $exception) {
$this->logger->error(
"Failed to send verify mail.",
["cause" => $exception, "recipient" => $arg1]
);
$mailer->getSMTPInstance()->reset();
}
break;
case "notify-death":
// TODO: Set name somehow
[$mailer->Subject, $mailer->Body] = $this->create_death_notification_email($arg2);
try {
$mailer->addAddress($arg1);
$mailer->send();
$stmt->execute();
} catch (Exception $exception) {
$this->logger->error(
"Failed to send death notification mail.",
["cause" => $exception, "recipient" => $arg1]
);
$mailer->getSMTPInstance()->reset();
}
}
$mailer->clearAddresses();
}
}
}