111 lines
4.9 KiB
PHP
111 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Services\Mail\MailCatalogue;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* A From address belongs to the account that signs in.
|
|
*
|
|
* Every purpose mailbox in this installation has its OWN SMTP account, and a
|
|
* mail server lets an account send only from the address it owns. mailcow
|
|
* answers anything else with "553 5.7.1 Sender address rejected: not owned by
|
|
* user no-reply@…" — and it is right to.
|
|
*
|
|
* So a mail that takes its From from a mailbox has to go out over that mailbox's
|
|
* mailer as well. Setting one and forgetting the other produces a mail that
|
|
* renders perfectly, queues without complaint, and never arrives: InvoiceMail
|
|
* and OrderConfirmationMail both addressed billing@ while going out over the
|
|
* default mailer's no-reply@ login, which means no customer had ever received an
|
|
* invoice mail or an order confirmation on a server that checks.
|
|
*
|
|
* The same gap opened a second time, one level up. `mail.route.<mailart>` moves
|
|
* ONE mail type to another mailbox — and it moved only the envelope: the
|
|
* mailable still named `cp_<purpose>`, so `security-block` on info@ went out
|
|
* with `From: info@` over no-reply@'s session. The pairing this file guards is
|
|
* therefore no longer "From purpose == mailer purpose" but "From key == mailer
|
|
* key", with MailCatalogue as the one place that maps a key to its purpose.
|
|
*/
|
|
it('gives every mail that takes a mailbox From the matching mailer', function () {
|
|
$offenders = [];
|
|
|
|
foreach (File::allFiles(app_path('Mail')) as $file) {
|
|
$code = $file->getContents();
|
|
|
|
// Only the ones that take their sender from a mailbox. A mailable with
|
|
// no mailbox From has nothing to mismatch.
|
|
if (! str_contains($code, 'mailboxEnvelope(') && ! str_contains($code, 'mailboxAddresses(')) {
|
|
continue;
|
|
}
|
|
|
|
preg_match('/mailbox(?:Envelope|Addresses)\(\s*MailPurpose::([A-Z_]+)/', $code, $from);
|
|
preg_match("/->mailer\(MailCatalogue::mailer\(('[a-z0-9-]+')/", $code, $keyed);
|
|
preg_match("/->mailer\('cp_'\.MailPurpose::([A-Z_]+)\)/", $code, $byPurpose);
|
|
|
|
$fromPurpose = $from[1] ?? null;
|
|
|
|
if ($fromPurpose === null) {
|
|
continue;
|
|
}
|
|
|
|
// The mailer names a mail type: the catalogue must agree that this type
|
|
// belongs to the purpose the envelope asks for. Otherwise the fallback
|
|
// when no route is set would move the sender without moving the login.
|
|
//
|
|
// A literal that is not itself a key is a PREFIX — DunningNoticeMail
|
|
// builds 'dunning-'.$level, one class for four catalogued types. Then
|
|
// every type behind that prefix has to agree, and there has to be at
|
|
// least one: a prefix nothing matches is a mailer that does not exist.
|
|
if (isset($keyed[1])) {
|
|
$literal = trim($keyed[1], "'");
|
|
$expected = constant('App\Services\Mail\MailPurpose::'.$fromPurpose);
|
|
|
|
$matching = array_filter(
|
|
MailCatalogue::all(),
|
|
fn (string $key) => $key === $literal || str_starts_with($key, $literal),
|
|
ARRAY_FILTER_USE_KEY,
|
|
);
|
|
|
|
if ($matching === []) {
|
|
$offenders[] = $file->getFilename().": mail key [{$literal}] matches nothing in MailCatalogue";
|
|
|
|
continue;
|
|
}
|
|
|
|
foreach ($matching as $key => $entry) {
|
|
if ($entry['purpose'] !== $expected) {
|
|
$offenders[] = $file->getFilename().": From {$fromPurpose}, mail key {$key} is catalogued as {$entry['purpose']}";
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// No mail key at all (ContactRequestMail): the purpose mailer is still
|
|
// the right answer, and it still has to match the From.
|
|
if (($byPurpose[1] ?? null) !== $fromPurpose) {
|
|
$offenders[] = $file->getFilename().': From '.$fromPurpose.', mailer '.($byPurpose[1] ?? 'DEFAULT');
|
|
}
|
|
}
|
|
|
|
expect($offenders)->toBe([]);
|
|
});
|
|
|
|
it('sends each purpose over its own mailer, not over whatever mail.default is', function () {
|
|
// The purpose mailers exist so that each mailbox authenticates as itself.
|
|
// A mailable naming no mailer silently uses mail.default, which on this
|
|
// installation is the plain smtp mailer with ONE account behind it.
|
|
foreach (App\Services\Mail\MailPurpose::ALL as $purpose) {
|
|
expect(config('mail.mailers.cp_'.$purpose))
|
|
->toBe(['transport' => 'mailbox', 'purpose' => $purpose]);
|
|
}
|
|
});
|
|
|
|
it('gives every catalogued mail type a mailer of its own', function () {
|
|
// Without the mailer, MailRoute would move the envelope and leave the SMTP
|
|
// session where it was — the exact failure the route was built to allow.
|
|
foreach (MailCatalogue::all() as $key => $entry) {
|
|
expect(config('mail.mailers.'.MailCatalogue::mailer($key)))
|
|
->toBe(['transport' => 'mailbox', 'purpose' => $entry['purpose'], 'mail_key' => $key]);
|
|
}
|
|
});
|