Mailer

Note

Not part of core. Install it separately:

composer require kinetis/mailer

Mail sending via Symfony\Component\Mailer. A single DSN selects the transport — SMTP, or any of Symfony’s own API-based bridges (Sendgrid, Mailgun, Postmark, SES, …).

use Kinetis\Mailer\MailerFactory;
use Symfony\Component\Mime\Email;

$mailer = MailerFactory::fromConfig($config);

$email = new Email()
    ->from('noreply@example.com')
    ->to('user@example.com')
    ->subject('Welcome!')
    ->text('Thanks for signing up.');

$mailer->send($email);

Configuring

MAILER_DSN=smtp://user:pass@smtp.example.com:587

Or, using an API-based transport instead — the scheme selects it:

MAILER_DSN=sendgrid+api://KEY@default
MAILER_TIMEOUT=30

MAILER_TIMEOUT is seconds, default 30. It bounds one API send twice over — as the idle timeout between bytes and as the total duration — and redirects are disabled, so the provider endpoint the DSN names is the only one contacted. Zero or a negative value is rejected when the mailer is built. SMTP ignores all three. Symfony’s SMTP socket instead inherits PHP’s default_socket_timeout for the connection and each blocking stream operation. One SMTP exchange performs several operations, so that setting is an inactivity bound, not a total deadline for the send.

Whichever scheme you use, composer require the matching Symfony bridge package too (symfony/sendgrid-mailer, symfony/mailgun-mailer, symfony/postmark-mailer, symfony/amazon-mailer, …) — kinetis/mailer has no dispatch logic of its own here; Symfony\Component\Mailer\Transport discovers whichever bridge classes are installed.

Important

Only the API-based transports are non-blocking. SMTP opens a raw socket directly (stream_socket_client()), with no Fiber-yield point — sending over SMTP blocks the worker for as long as the send takes. A blocking send also gives the worker no chance to renew the job’s reservation (see Queue), and neither a visibility timeout nor a shutdown grace period can be set high enough to guarantee an SMTP send finished inside it, so at-least-once handling must stay safe when a reservation expires or the process is killed. See Queueing mail below for the practical fix, whichever transport you choose.

Warning

A Symfony HTTP transport exception carries the provider’s own debug data, which can include the raw request headers — and therefore the API key that authenticated the send. Kinetis never reads that data; if your own exception handler does, do not log it.

Named connections

$transactional = MailerFactory::fromConfig($config, 'transactional');
MAILER_TRANSACTIONAL_DSN=sendgrid+api://KEY@default
MAILER_TRANSACTIONAL_TIMEOUT=10

Same convention as everywhere else in Kinetis (see Configuration): 'default' reads the plain MAILER_DSN/MAILER_TIMEOUT above, and any other name reads MAILER_{NAME}_DSN/MAILER_{NAME}_TIMEOUT instead.

Queueing mail

Sending mail from inside a request means the client waits for it — and on SMTP that wait blocks the worker. kinetis/mailer needs to provide nothing for this: a queue job that constructor-injects MailerInterface already gets it, with no extra code in either package.

use Kinetis\Queue\Job;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final readonly class SendWelcomeEmailJob implements Job
{
    public function __construct(
        public string $toEmail,
    ) {}

    public function handle(MailerInterface $mailer): void
    {
        $email = new Email()
            ->from('noreply@example.com')
            ->to($this->toEmail)
            ->subject('Welcome!')
            ->text('Thanks for signing up.');

        $mailer->send($email);
    }
}
$queue->push(new SendWelcomeEmailJob($user->email));

With MAILER_DSN set, MailerInterface is already bound — installing the package registers it — so any job’s handle() method can depend on it exactly like a repository or any other service, with nothing in bootstrap.php at all. The transport is built while the package registers, so a malformed DSN or a missing bridge package fails at boot rather than on the first job that sends. An application’s own bootstrap.php runs after that and still replaces the binding.

A slow SMTP send then occupies one queue worker’s Fiber for one job instead of the worker handling an HTTP request.

See also

  • HTTP Client — the non-blocking HTTP client every API-based transport runs through.

  • Queue — job queues, retries, and named connections in full.

  • Configuration — the named-connection convention used above.