std/net/smtp

Standard Library source code

low-level mail delivery.

Module

Name
std/net/smtp
Area
Standard Library
Source
modules/std/net/smtp.zzm
=encoding utf8

=head1 NAME

std/net/smtp - low-level mail delivery.

=head1 SYNOPSIS

  from std/net/smtp import Mailer, MailResult;

  let headers := new PairList();
  headers.add( "From", "sender@example.test" );
  headers.add( "To", "recipient@example.test" );
  headers.add( "Subject", "Example" );
  headers.add( "Message-ID", "<example-1@example.test>" );

  let mailer := new Mailer(
    transport: "smtp",
    host: "127.0.0.1",
    port: 2525
  );

  let result := mailer.send(
    "sender@example.test",
    [ "recipient@example.test" ],
    headers,
    to_binary( "Hello\r\n" )
  );

=head1 IMPLEMENTATION SUPPORT

This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
Electron. It is not supported by zuzu-js in the browser.

=head1 DESCRIPTION

This runtime-supported module provides a deliberately low-level mail
delivery API. It works with an explicit envelope sender, explicit
envelope recipients, ordered message headers, and a raw C<BinaryString>
body.

It does not construct MIME messages, encode display names, generate a
C<Message-ID>, derive recipients from C<To>, C<Cc>, or C<Bcc> headers,
or add attachments. Higher-level composition modules should build the
headers and body, then call this module.

=head1 EXPORTS

=head2 Classes

=over

=item C<Mailer>

Configured sender class.

=over

=item C<< Mailer.capabilities() >>

Parameters: none. Returns: C<Dict>. Reports backend delivery, TLS,
authentication, and async support.

=item C<< Mailer({ transport?: String, host?: String, port?: Number, ... }) >>

Constructs a sender from named options. Returns: C<Mailer>.

=item C<< mailer.send(envelope_from, envelope_to, headers, body, options := {}) >>

Parameters: C<envelope_from> is the sender address, C<envelope_to> is a
string or array of recipient addresses, C<headers> is a C<PairList>,
C<body> is a C<BinaryString>, and C<options> controls sending. Returns:
C<MailResult>. Sends one message.

=item C<< mailer.send_async(envelope_from, envelope_to, headers, body, options := {}) >>

Parameters: same as C<send>. Returns: C<Task>. Asynchronously sends one
message.

=back

=item C<MailResult>

Result object returned by successful deliveries.

=over

=item C<< result.transport() >>

Parameters: none. Returns: C<String>. Returns the delivery transport.

=item C<< result.accepted() >>

Parameters: none. Returns: C<Array>. Returns accepted envelope
recipients.

=item C<< result.rejected() >>

Parameters: none. Returns: C<Array>. Returns rejected envelope
recipients.

=item C<< result.message_id() >>

Parameters: none. Returns: C<String> or C<null>. Returns the supplied
C<Message-ID> header value when present.

=item C<< result.response() >>

Parameters: none. Returns: C<String> or C<null>. Returns the backend
delivery response text.

=item C<< result.to_Dict() >>

Parameters: none. Returns: C<Dict>. Converts the result to a dictionary.

=back

=back

The module does not export a module-level C<capabilities()> function.
Use C<< Mailer.capabilities() >>.

=head1 CAPABILITIES

C<< Mailer.capabilities() >> returns a C<Dict> with stable keys:

  {
    smtp: true,
    sendmail: true,
    tls: true,
    starttls: true,
    auth: [ "plain", "login", "xoauth2" ],
    async: true,
  }

Backends report C<false> or an empty array for features they cannot
honestly provide. Browser hosts are importable but report no real
delivery support.

=head1 MAILER OPTIONS

Create a sender with named options:

  let mailer := new Mailer(
    transport: "sendmail",
    sendmail_path: "/usr/sbin/sendmail"
  );

Supported options include:

=over

=item C<transport>

C<"smtp"> or C<"sendmail">. Defaults to C<"smtp">.

=item C<host>, C<port>, C<timeout>

SMTP host, port, and command timeout. Defaults are C<localhost>, C<25>,
and C<30> seconds.

=item C<submission>

Uses submission-style defaults: port C<587> and STARTTLS requested.
Backends without STARTTLS support will reject sending clearly.

=item C<tls>, C<starttls>, C<tls_verify>, C<tls_server_name>

TLS policy fields. C<tls_verify> defaults to true.

=item C<username>, C<password>, C<auth>

Authentication fields. C<auth> may be C<plain>, C<login>, or
C<xoauth2>. Authentication on a plaintext connection requires explicit
C<allow_insecure_auth: true>.

=item C<smtputf8>

Permit non-ASCII envelope addresses only when the backend and server can
honour SMTPUTF8.

=item C<reject_partial>

For SMTP, throw if any recipient is rejected. The default returns a
C<MailResult> when at least one recipient is accepted.

=item C<sendmail_path>, C<sendmail_args>

Sendmail-compatible binary path and extra fixed argument-vector items.
The backend invokes:

  sendmail_path sendmail_args... -i -f ENVELOPE_FROM RECIPIENT...

No shell interpolation is used.

=back

=head1 SENDING

  mailer.send(envelope_from, envelope_to, headers, body, options := {})
  mailer.send_async(envelope_from, envelope_to, headers, body, options := {})

C<envelope_to> may be a single string or an array of strings. These are
the only delivery recipients. Header fields are never used as envelope
recipients.

C<headers> must be a C<PairList>, preserving order and duplicate fields.
C<Dict> headers are rejected. Header names must be strict RFC 5322 field
name tokens with no colon or control characters. Header values must be
C<String> or C<BinaryString> values and must not contain CR or LF.

C<body> must be a C<BinaryString>. Passing a C<String> throws a clear
type error so callers do not accidentally depend on host text encoding.

Serialization is:

=over

=item 1.

Headers in C<PairList> order using CRLF line endings.

=item 2.

One blank CRLF separator.

=item 3.

The body bytes exactly as supplied.

=back

SMTP dot-stuffing is a wire encoding detail and does not mutate caller
bytes.

=head1 MAIL RESULT

A successful send returns a C<MailResult> with public fields:

  {
    transport: "smtp",
    accepted: [ "recipient@example.test" ],
    rejected: [],
    message_id: "<example-1@example.test>",
    response: "250 queued",
  }

C<message_id> is copied from a supplied C<Message-ID> header when one is
present. This module never generates one automatically.

=head1 COPYRIGHT AND LICENCE

B<< std/net/smtp >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.