CluPilotCloud/tests/Feature/MailSenderOwnershipTest.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]);
}
});