Search by

hydrakit / mail

whleucka

Outgoing mail behind one transport contract: SMTP for delivery, log and array transports so tests and local development never send.

v0.31.2 2026-10-07 19:47 UTC

README

Part of the Hydra PHP framework. Documentation: hydra.williamhleucka.com/docs.

Read-only mirror. hydrakit/mail is developed in hydra-foundation/hydra under packages/mail, and republished here on every push. A commit pushed to this repository is overwritten by the next one; issues are disabled for that reason, and a pull request opened here cannot be merged. Both belong upstream.

Outgoing mail behind one transport contract. SmtpTransport delivers; LogTransport writes each message to the log with its links intact; ArrayTransport keeps them in memory. Only the first sends anything, so tests and local development never do.

$mailer->send(Message::make()
    ->to('ada@example.com', 'Ada')
    ->subject('Reset your password')
    ->text("Follow this link: {$url}")
    ->html("<p><a href=\"{$url}\">Reset your password</a></p>"));

A message with no sender gets MAIL_FROM_ADDRESS. Every recipient must be accepted before the message is sent, so a refused address fails the whole send rather than delivering to some recipients. TLS is never skipped: with MAIL_ENCRYPTION=tls a server that does not offer STARTTLS is an error, and credentials are refused outright with MAIL_ENCRYPTION=none.

Setting Default
MAIL_TRANSPORT smtp smtp, log or array
MAIL_HOST required for smtp
MAIL_PORT 587
MAIL_ENCRYPTION tls tls (STARTTLS), ssl (implicit TLS) or none
MAIL_USERNAME, MAIL_PASSWORD AUTH PLAIN, or LOGIN when PLAIN is not offered
MAIL_FROM_ADDRESS, MAIL_FROM_NAME the default sender
MAIL_TIMEOUT 10 seconds, for the connection and each reply
MAIL_EHLO_DOMAIN localhost the name this client greets the server with

When the app binds a PSR-14 EventDispatcherInterface, every message a transport accepts is announced as Events\MessageSent, carrying the message as sent (sender filled in) and the transport's name. A send that throws announces nothing. The package stores nothing; an app that wants a log of sent mail listens:

$listeners->listen(MessageSent::class, function (MessageSent $sent): void {
    // $sent->message->getTo(), ->getSubject(), $sent->transport …
});

A listener that throws reaches the caller of send() after the mail went out, so one that records should catch and log its own failures. FakeMailer dispatches nothing.

In tests, register Testing\FakeMailServiceProvider after MailServiceProvider and assert on the FakeMailer it binds:

$mail = $container->get(FakeMailer::class);
$mail->assertSentTo('ada@example.com');
$mail->assertSent(fn (Message $m) => $m->getSubject() === 'Reset your password', times: 1);

Not yet: attachments, custom headers, and SMTPUTF8 (addresses are ASCII; names and subjects may be anything).