Skip to content

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.

Terminal window
composer require marko/mail-smtp

This automatically installs marko/mail.

Set the mail driver to smtp in your config:

config/mail.php
use Marko\Config\Env;
return [
'driver' => 'smtp',
'from' => [
'address' => Env::string('MAIL_FROM_ADDRESS', '[email protected]'),
'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,
],
];
OptionDefaultDescription
hostlocalhostSMTP server hostname
port587SMTP server port
encryptiontlsEncryption method --- tls (STARTTLS), ssl (implicit TLS), or null for none
usernamenullSMTP authentication username
passwordnullSMTP authentication password
auth_modeloginAuthentication mode --- login or plain
timeout30Connection timeout in seconds

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 port 587) opens a plain TCP connection, sends EHLO, upgrades the connection with STARTTLS, then sends EHLO again before authenticating. Nothing is sent in plain text after the upgrade.
  • ssl (implicit TLS, usually port 465) negotiates TLS as soon as the connection opens, before the server’s greeting. No STARTTLS is sent.

Using ssl against a STARTTLS port, or tls against an implicit-TLS port, fails with a TransportException whose reason names the handshake error.

Bind the mailer interface in your module.php:

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)
->from('[email protected]', 'Store')
->subject('Your order has shipped')
->html("<p>Tracking: $trackingNumber</p>");
$this->mailer->send($message);
}
}

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]>

A CR, LF or NUL byte in an address or display name throws MessageException (header injection) before anything is encoded.

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
}

Implements MailerInterface. See marko/mail for the full interface contract.

MethodDescription
send(Message $message): boolBuild and send a Message over SMTP --- handles headers, MIME encoding, and attachments
sendRaw(string $to, string $raw): boolSend 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

Low-level SMTP protocol transport --- manages the socket connection, TLS negotiation, authentication, and envelope commands.

MethodDescription
connect(string $host, int $port, ?string $encryption = null): voidOpen a socket connection to the SMTP server
ehlo(string $hostname): arraySend the EHLO greeting and return server capabilities
startTls(): voidUpgrade the connection to TLS encryption
authenticate(string $username, string $password, string $mode = 'LOGIN'): voidAuthenticate with the server using LOGIN or PLAIN
mailFrom(string $address): voidSet the envelope sender address
rcptTo(string $address): voidAdd an envelope recipient address
data(string $content): voidSend the message content
quit(): voidClose 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 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.

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).

MethodRequiredDescription
host(): stringYesSMTP server hostname --- throws if missing
port(): intYesSMTP server port --- throws if missing
encryption(): stringYesEncryption method ('tls', 'ssl', or 'none') --- throws if missing
timeout(): intYesConnection timeout in seconds --- throws if missing
authMode(): stringYesAuthentication mode ('login' or 'plain') --- throws if missing
username(): ?stringNoAuthentication username; null for no-auth SMTP
password(): ?stringNoAuthentication password; null for no-auth SMTP