NAME

Punk::Mailer::Transport - the transports, and what one has to be

SYNOPSIS

Punk::Mailer->new(transport => 'smtp',     smtp     => { host => 'mail.example.com', ... });
Punk::Mailer->new(transport => 'resend',   resend   => { api_key => $key });
Punk::Mailer->new(transport => 'sendmail', sendmail => { command => [ '/usr/sbin/sendmail', '-i' ] });
Punk::Mailer->new(transport => 'capture',  capture  => { dir => 'var/mail' });
Punk::Mailer->new(transport => 'log');
Punk::Mailer->new(transport => 'My::Transport', options => { ... });

DESCRIPTION

A transport takes a message and its envelope and returns a Punk::Mailer::Result. The engine builds one at new, which is where every option is checked - an unknown option, a missing credential, a command given as a string - so nothing is deferred to the first send.

THE SHIPPED TRANSPORTS

capture

capture => { dir => 'var/mail', result => 'accepted' }

For tests and development. Every delivery is kept on messages as { spec, envelope, bytes, result }, so a test reads what would have gone out; clear empties it. With dir, each message is also written as one <epoch>.<seq>.<pid>.eml under dir/new/, which a mail client can open. result scripts the verdict - accepted, deferred, rejected or failed - so a test can exercise its error branch without a server.

log

log => { to => $fh | \&code }

The honest fallback: the message goes to STDERR (or to) and the Result is unsent. It exists so a development box says transport => 'log' and sees the mail, rather than having no transport and wondering where it went.

sendmail

sendmail => { command => [ '/usr/sbin/sendmail', '-i' ] }

A local MTA's command line. command is a list of arguments and is run without a shell; a string croaks at new. -f with the envelope sender and then the envelope recipients are appended, so bcc works without -t. The message streams straight into the command's standard input. A non-zero exit is a failed Result carrying the exit status; 127 means the command was not found.

The default command is ['/usr/sbin/sendmail', '-i']. Keep the -i in your own: without it a line holding a single . ends the message for most sendmails.

resend

resend => { api_key => $key, timeout => 10, max_attachment => 8 * 1024 * 1024 }

Resend's HTTP API, one JSON POST per message. 2xx is accepted with Resend's id; 429 and 5xx are deferred; any other 4xx is rejected with Resend's message; no answer is failed. An attachment is sent as one base64 string, which is the one place a file named by path is read into memory, so max_attachment (default 8MB) refuses a larger one locally as rejected rather than uploading it. url points the transport elsewhere, for a proxy or a test.

smtp

smtp => {
    host     => 'mail.example.com',
    port     => 587,                 # the default for the tls mode
    tls      => 'starttls',          # starttls | implicit | none
    verify   => 1,
    timeout  => 15,
    username => 'ops@example.com',
    password => $password,
    name     => 'app.example.com',   # the EHLO name; the From domain by default
};

RFC 5321, with STARTTLS on 587, implicit TLS on 465, or plaintext on 25. TLS comes from Fetch's client configuration, so the system's certificate store applies and verify (on by default) checks the server's certificate and hostname. timeout bounds every read and write on the connection.

The client asks for STARTTLS only after the greeting, upgrades only on a 220, and sends EHLO again afterwards - the capabilities a server announced in plaintext are not trusted. A server that does not offer STARTTLS when tls is starttls is a failed Result, never a silent plaintext session.

AUTH PLAIN is used when offered, AUTH LOGIN otherwise. A password is never sent over plaintext: tls = 'none'> with a username croaks at new unless insecure_auth = 1> is also given, which is a way of saying so in writing.

When the server announces SIZE, the message's size is sent with MAIL FROM - known from arithmetic, without reading an attachment - and a message already over the limit is rejected locally with 552. The message itself streams from the builder onto the socket, dot-stuffed, so an attachment never sits in memory.

The Result: the 250 after DATA is accepted, with the server's text (usually a queue id) in message; a 4xx anywhere is deferred; a 5xx is rejected; a lost connection, a timeout, a failed handshake or an unparseable reply is failed, and the message names the phase - greeting, ehlo, starttls, auth, mail, rcpt, data. Every recipient's verdict is in recipients; a message some of whose recipients were refused while at least one was accepted is accepted with the refusals recorded, which is what the server did. The enhanced status code is kept when the server sent one.

WRITING ONE

package My::Transport;
sub new     { my ($class, $opts) = @_; ... bless {...}, $class }
sub deliver { my ($self, $spec, $envelope) = @_; ... return $result }
sub name    { 'mine' }

deliver receives the message hashref with the engine's defaults filled in and already validated, and the envelope { from => $addr, to => [ @addrs ] }. It must return a Punk::Mailer::Result; build the bytes with Punk::Mailer->build($spec) or stream them with build_to. Return a Result for anything that went wrong on the wire, and die only for a bug.

SEE ALSO

Punk::Mailer, Punk::Mailer::Result.