mj-zana / communication
Email for Zana applications: messages, configured senders, Symfony Mailer transports and a recorder for tests
v2.1.0
2026-10-25 20:38 UTC
Requires
- php: ^8.5
- mj-zana/core: ^2.1
- symfony/mailer: ^7.4 || ^8.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Email for Zana applications: immutable messages, a configured sender, Symfony Mailer transports, and a recorder for tests.
composer require mj-zana/communication
Setup
Register CommunicationServiceProvider and configure the sender:
// config/mail.php return [ 'dsn' => 'smtp://user:pass@mail.example.com:587', // memory:// records instead (the default) 'from' => ['address' => 'website@example.com', 'name' => 'Example'], 'reply_to' => 'contact@example.com', // optional default Reply-To ];
Keep the DSN secret: set it from the environment, e.g.
APP_MAIL__DSN=smtp://....
Sending
$mailer->send(Message::create() ->to('contact@example.com') ->replyTo($visitor->email) ->subject('New message from the website') ->text($body));
Rules
- From comes from configuration. A message without
from()getsmail.from. Any other From must be listed inmail.allowed_from. A visitor's address goes in Reply-To, never in From: this keeps SPF and DMARC aligned and prevents spoofing. - Required: a recipient, a subject, and a text or HTML body.
- No header injection: line breaks in subjects, header values and names are refused.
- Transport failures become
MailException.
Testing
With mail.dsn: memory://, the mailer is a MailRecorder, which applies the
same rules:
$recorder = $container->get(MailRecorder::class); $this->assertCount(1, $recorder->sent(fn (Message $m): bool => $m->subject === 'Welcome'));
mj-zana/testing adds assertMailSent() and assertNoMailSent().
License
MIT