Skip to content

SMTP

When it's time to send real email, the smtp transport speaks SMTP directly to your mail server. It handles the full conversation — EHLO, authentication, the envelope, and the message body — and supports STARTTLS, implicit SSL/TLS, LOGIN, PLAIN and XOAUTH2 authentication, and connection reuse across a batch.

Configuring an SMTP mailer

Transports are wired through configuration, not code. Add a named mailer to your Config\Mailer and point it at the smtp transport, then give it your server's settings:

<?php

public array $mailers = [
    'smtp' => [
        'transport'  => 'smtp',
        'host'       => 'smtp.example.com',
        'port'       => 587,
        'authType'   => 'login',
        'encryption' => 'tls',
        'timeout'    => 30,
        'keepAlive'  => false,
    ],
];

public function __construct()
{
    parent::__construct();

    // PHP doesn't allow function calls in property defaults, so read secrets here.
    $this->mailers['smtp']['username'] = env('SMTP_USER');
    $this->mailers['smtp']['password'] = env('SMTP_PASS');
}

Keep credentials out of committed config

Source username and password from your environment with env('SMTP_USER') / env('SMTP_PASS') (backed by .env) in the config's constructor, as shown above — never hardcode them in Config\Mailer, which is committed to version control. Add the variables to .env (which is git-ignored) and to your deployment's secret store.

Send through it by name, exactly like any other mailer:

<?php

use Myth\Postal\Email;

$email = (new Email())
    ->from('you@example.com', 'Your Name')
    ->to('user@example.com')
    ->subject('Welcome aboard')
    ->html('<p>Glad to have you with us.</p>');

$result = service('mailer')->mailer('smtp')->send($email);

To make SMTP the default for every send, set public string $default = 'smtp'; and call service('mailer')->send($email) with no name.

Configuration reference

Every key in the mailer array beyond transport is an SMTP setting. Anything you omit falls back to a sensible default.

Setting Default What it does
host localhost The SMTP server hostname or IP.
port 25 The server port. Use 587 for STARTTLS, 465 for implicit SSL.
username (none) The auth username.
password (none) The auth password, or the OAuth2 access token for xoauth2.
authType login when username and password are both set, otherwise (none) login, plain, or xoauth2.
encryption (none) tls for a STARTTLS upgrade, ssl for an implicit TLS connection.
timeout 30 Connection timeout in seconds.
keepAlive false Reuse one connection across multiple sends. See below.
helo (system hostname) The hostname announced in the EHLO command. Defaults to gethostname().
dsn (none) Delivery Status Notification options. See below.

Authentication

Pick the method your provider expects with authType:

  • login — the classic challenge/response exchange. The most widely supported.
  • plain — username and password sent in a single base64 token.
  • xoauth2 — OAuth2 bearer tokens. Put your access token in password and the account in username.
<?php

'authType' => 'xoauth2',
'username' => 'you@gmail.com',
'password' => $oauthAccessToken,

When username and password are both set and authType is omitted, login is used. Leave out the credentials for servers that don't require authentication (a local relay, for instance).

Encryption

Two encryption modes, matching the two common ports:

<?php

'port'       => 587,
'encryption' => 'tls',
Connects in the clear, then upgrades the live connection to TLS with STARTTLS.

<?php

'port'       => 465,
'encryption' => 'ssl',
Negotiates TLS immediately, before the SMTP conversation begins.

Port 465 is implicit TLS automatically

Set port => 465 and Postal uses implicit TLS even if you leave encryption unset, per RFC 8314. Setting encryption => 'ssl' is still fine — and is what forces implicit TLS on any other port.

Keep-alive

By default each send opens a fresh connection and closes it with QUIT. When you're sending a batch in one request, that handshake-per-message overhead adds up. Set keepAlive to true to reuse a single connection:

<?php

$mailer = service('mailer')->mailer('smtp');

foreach ($users as $user) {
    $mailer->send($welcomeFor($user));
}

The first send connects and authenticates; each later send issues a quick RSET and reuses the open connection. If the server has dropped an idle connection in the meantime, the transport reconnects transparently rather than failing the send.

One connection per mailer instance

Keep-alive lives on the resolved mailer instance, which is cached per name. Grab the mailer once and reuse it for the batch, as shown above.

Delivery Status Notifications

SMTP can ask the server to report back on delivery with DSN. Configure it with the dsn key:

<?php

'dsn' => [
    'notify' => 'FAILURE,DELAY',
    'ret'    => 'HDRS',
    'envid'  => 'order-4821',
],
  • notify — when to report: any of SUCCESS, FAILURE, DELAY, or NEVER.
  • ret — how much of the message to return on a bounce: FULL or HDRS (headers only).
  • envid — an envelope identifier echoed back in the notification, handy for correlation.

DSN parameters are only sent when the server advertises DSN support in its EHLO reply. If it doesn't, they're silently skipped — your mail still goes out.

When a send fails

Connection and protocol errors don't throw. The transport catches them, drops the connection so the next send starts clean, and returns a failed SendResult with the reason in error:

Could not connect to SMTP server smtp.example.com:587: Connection refused
Lost connection to SMTP server: read timed out
Unexpected reply to "RCPT TO:<bob@example.com>": 550 5.1.1 User unknown

Internally these are Myth\Postal\Exceptions\SmtpException, a subclass of PostalException, but none of them reach your code. ping() reports the same problems by returning false.

Testing without a real server

The transport talks through a small seam, Myth\Postal\Transport\SmtpSocket, rather than touching the network directly. The shipped StreamSocket is the real implementation that opens a TCP/TLS stream to your server.

That seam exists so the SMTP conversation is fully testable. Pass your own SmtpSocket as the transport's second argument to drive the protocol in a unit test:

<?php

use Myth\Postal\Transport\SmtpTransport;

$transport = new SmtpTransport(['host' => 'mail.test'], $myFakeSocket);

Postal's own suite uses a scripted double for exactly this. That double ships in the package's tests/ directory — it's internal test plumbing, not public API, so don't rely on it from your application.

Security

Handle credentials and untrusted addresses with care

  • Source secrets from the environment. Read username/password from .env via env() in the config's constructor and never commit them in Config\Mailer. See the warning above.
  • SMTP command injection is blocked. CR/LF is stripped from every command line, so an address, HELO name, or DSN value can't smuggle extra SMTP commands. Still, validate addresses at your application boundary.
  • Credentials never reach your logs. A failed AUTH reports against a safe label, so base64-encoded usernames and passwords stay out of exception messages and SendResult errors.
  • Use encryption for authenticated sends. login and plain put your credentials on the wire. Pair them with encryption => 'tls' or 'ssl' so they're never sent in the clear.

Next steps

  • Log — inspect messages in development without sending them
  • MIME Rendering — what the message body looks like on the wire