refactor(mail): queue a message through a typed dto
Every producer wrote the queue row by hand: the recipient, the subject and the template variables shared one untyped array, so a misspelled key was found by the cron a minute later, if at all. A message is now queued through MailQueueInterface::push() with a QueuedEmailDTO, and the six call sites in registration, profile and contacts no longer touch the model at all.
Putting a message in and taking messages out are separate interfaces on purpose: a module needs neither claiming nor retrying, and the sender needs no producer methods.
The addresses of a message are validated as it is queued, so a broken one is reported where the mistake was made instead of exhausting the delivery attempts first. The contact form catches that: its notification address comes from the settings of the site rather than a validated form, and a typo there must not fail the visitor whose message was already saved.
The dto also closes a hole of the old shape: template variables are merged last, so a variable named email_to can no longer redirect the message.
Putting a message in and taking messages out are separate interfaces on purpose: a module needs neither claiming nor retrying, and the sender needs no producer methods.
The addresses of a message are validated as it is queued, so a broken one is reported where the mistake was made instead of exhausting the delivery attempts first. The contact form catches that: its notification address comes from the settings of the site rather than a validated form, and a typo there must not fail the visitor whose message was already saved.
The dto also closes a hole of the old shape: template variables are merged last, so a variable named email_to can no longer redirect the message.