marko/mail-smtp
SMTP mail driver --- delivers emails over SMTP with TLS, authentication, and MIME encoding. Handles TLS encryption, LOGIN/PLAIN authentication, multipart MIME messages (HTML + text + attachments), and inline images. Configuration comes from config/mail.php under the smtp key.
Implements MailerInterface from marko/mail.
Installation
Section titled “Installation”composer require marko/mail-smtpThis automatically installs marko/mail.
Configuration
Section titled “Configuration”Set the mail driver to smtp in your config:
use Marko\Config\Env;
return [ 'driver' => 'smtp', 'from' => [ 'name' => Env::string('MAIL_FROM_NAME', 'My App'), ], 'smtp' => [ 'host' => Env::string('MAIL_HOST', 'localhost'), 'port' => Env::int('MAIL_PORT', 587, min: 1, max: 65535), 'encryption' => Env::nullableString('MAIL_ENCRYPTION', 'tls'), 'username' => Env::nullableString('MAIL_USERNAME'), 'password' => Env::nullableString('MAIL_PASSWORD'), 'auth_mode' => 'login', // 'login' or 'plain' 'timeout' => 30, ],];| Option | Default | Description |
|---|---|---|
host | localhost | SMTP server hostname |
port | 587 | SMTP server port |
encryption | tls | Encryption method --- tls (STARTTLS), ssl (implicit TLS), or null for none |
username | null | SMTP authentication username |
password | null | SMTP authentication password |
auth_mode | login | Authentication mode --- login or plain |
timeout | 30 | Connection timeout in seconds |
Choosing tls or ssl
Section titled “Choosing tls or ssl”The two encryption modes match the two ways mail servers offer TLS, so pick the one your provider documents for the port you use:
tls(STARTTLS, usually port587) opens a plain TCP connection, sendsEHLO, upgrades the connection withSTARTTLS, then sendsEHLOagain before authenticating. Nothing is sent in plain text after the upgrade.ssl(implicit TLS, usually port465) negotiates TLS as soon as the connection opens, before the server’s greeting. NoSTARTTLSis sent.
Using ssl against a STARTTLS port, or tls against an implicit-TLS port, fails with a TransportException whose reason names the handshake error.
Automatic via Binding
Section titled “Automatic via Binding”Bind the mailer interface in your module.php:
use Marko\Mail\Contracts\MailerInterface;use Marko\Mail\Smtp\SmtpMailer;
return [ 'bindings' => [ MailerInterface::class => SmtpMailer::class, ],];Then inject MailerInterface and send emails:
use Marko\Mail\Contracts\MailerInterface;use Marko\Mail\Message;
class OrderNotifier{ public function __construct( private MailerInterface $mailer, ) {}
public function notifyShipment( string $email, string $trackingNumber, ): void { $message = Message::create() ->to($email) ->subject('Your order has shipped') ->html("<p>Tracking: $trackingNumber</p>");
$this->mailer->send($message); }}Message Format
Section titled “Message Format”send() builds an RFC 5322 message: the header section ends with an empty line before the body, so multipart messages are parsed as MIME parts rather than shown as raw source. Header values are 7-bit ASCII. A non-ASCII subject or display name in From, To, Cc or Reply-To is RFC 2047 encoded (=?UTF-8?B?...?=). An ASCII name is sent as is when it is plain words, and as an RFC 5322 quoted-string (with \ and " escaped) when it contains specials such as ,, <, >, ;, @ or ". A user-supplied name like x <[email protected]>, y therefore stays one display name rather than adding recipients:
From: =?UTF-8?B?UsOpbXk=?= <[email protected]>To: Store <[email protected]>Reply-To: "x <[email protected]>, y" <[email protected]>A CR, LF or NUL byte in an address or display name throws MessageException (header injection) before anything is encoded.
Customization
Section titled “Customization”Extend SmtpMailer via Preference to customize message building:
use Marko\Core\Attributes\Preference;use Marko\Mail\Smtp\SmtpMailer;
#[Preference(replaces: SmtpMailer::class)]class CustomSmtpMailer extends SmtpMailer{ // Custom SMTP behavior}API Reference
Section titled “API Reference”SmtpMailer
Section titled “SmtpMailer”Implements MailerInterface. See marko/mail for the full interface contract.
| Method | Description |
|---|---|
send(Message $message): bool | Build and send a Message over SMTP --- handles headers, MIME encoding, and attachments |
sendRaw(string $to, string $raw): bool | Send a pre-built raw message string to the given recipient; the envelope sender is the address in the message’s From: header (inside <...> when it has a display name). $to is validated as an Address first, so an invalid address throws MessageException before anything is sent |
SmtpTransport
Section titled “SmtpTransport”Low-level SMTP protocol transport --- manages the socket connection, TLS negotiation, authentication, and envelope commands.
| Method | Description |
|---|---|
connect(string $host, int $port, ?string $encryption = null): void | Open a socket connection to the SMTP server |
ehlo(string $hostname): array | Send the EHLO greeting and return server capabilities |
startTls(): void | Upgrade the connection to TLS encryption |
authenticate(string $username, string $password, string $mode = 'LOGIN'): void | Authenticate with the server using LOGIN or PLAIN |
mailFrom(string $address): void | Set the envelope sender address |
rcptTo(string $address): void | Add an envelope recipient address |
data(string $content): void | Send the message content |
quit(): void | Close the SMTP session and disconnect |
mailFrom() and rcptTo() throw TransportException::commandInjection() when the address contains a CR, LF or NUL byte, before the command is written, so a crafted address cannot inject further SMTP commands or recipients.
Connection and TLS Errors
Section titled “Connection and TLS Errors”Connection and STARTTLS failures throw TransportException (from marko/mail). The context includes the operating system’s or OpenSSL’s reason, so you can tell the causes apart. No raw PHP warning is emitted.
Failed to connect to mail server.Could not establish connection to smtp.example.com:587 (Connection refused)Typical reasons are Connection refused (nothing listening on that port), Connection timed out (a firewall dropping packets), getaddrinfo ... failed (the host name doesn’t resolve) and, with ssl://, certificate verify failed. A failed STARTTLS handshake throws TransportException::tlsFailed(), and its context includes the OpenSSL error, for example SSL routines::wrong version number when the port doesn’t speak TLS.
TransportException::connectionFailed($host, $port, ?string $reason = null) and tlsFailed($host, ?string $reason = null) take the reason as an optional last argument, so custom transports can pass one too.
SmtpConfig
Section titled “SmtpConfig”Reads SMTP settings from the smtp key in config/mail.php via MailConfig. The required keys throw MailException if absent; username and password return null when not set (no-auth SMTP).
| Method | Required | Description |
|---|---|---|
host(): string | Yes | SMTP server hostname --- throws if missing |
port(): int | Yes | SMTP server port --- throws if missing |
encryption(): string | Yes | Encryption method ('tls', 'ssl', or 'none') --- throws if missing |
timeout(): int | Yes | Connection timeout in seconds --- throws if missing |
authMode(): string | Yes | Authentication mode ('login' or 'plain') --- throws if missing |
username(): ?string | No | Authentication username; null for no-auth SMTP |
password(): ?string | No | Authentication password; null for no-auth SMTP |