~/bend-docscommunity

core.bend checks

raw source on the hub · import 0x00e7af2de246c3a4c341d9ee49e68747/core.bend as Core

The pure core of the SMTP client: the message, the options, the commands, what a server's EHLO means, the plan for a message, and the outcome. Nothing here does IO or calls foreign code, which is what lets LAWS.bend state laws about it and PROOF.bend prove them; smtp.bend is the dialog over the network, built on this.

7 imports
import Base
import ./text.bend as T
import ./reply.bend as R
import ./mime.bend as M
import ./idna.bend as I
import ./addr.bend as A
import ./md5.bend as D

Types

type Hdr source · line 15 · raw

Data

An extra header field, as "X-Mailer: bend-smtp".

type Mail source · line 20 · raw

Data

A message: sender, To, Cc, Bcc (never in the headers), Reply-To, subject, plain text, HTML ("" for none), attachments and extra headers.

type Mode source · line 28 · raw

Data

Plain: no TLS. Tls: TLS from the first byte (port 465). StartTls: plain, then STARTTLS, and the talk refuses to go on without it (port 587).

type Auth source · line 34 · raw

Data

The AUTH mechanism asked for; AAuto picks from what the server offers.

type Kv source · line 51 · raw

Data

Where and how to send. cafile "" trusts the system's store; user "" sends no AUTH; helo "" asks the host (Net.helo); token is an OAuth 2 access token ("" for none); debug writes the dialog to stderr, secrets and the message's text left out. more holds the rest by name (Opts.opt): "lmtp" (speak LMTP, RFC 2033), "notify", "ret" and "envid" (delivery status notifications, RFC 3461), "no-pipelining", "chunking" (send with BDAT, RFC 3030), "proxy" (socks5://[user:pass@]host:port), "cert" and "key" (a TLS client certificate), "dkim-domain", "dkim-selector" and "dkim-key".

type Opts source · line 54 · raw

Data

type Caps source · line 61 · raw

Data

What EHLO announced: STARTTLS, the AUTH mechanisms, SIZE (0: none), SMTPUTF8, 8BITMIME, and every extension's keyword (upper-cased).

type Mech source · line 66 · raw

Data

The mechanism to use, or why there is none.

type Sent source · line 77 · raw

Data

How one message went: code 0 when the server took it, else the reply (or the reason) that stopped it; refused lists the recipients the server did not take, the others having got it.

type Batch source · line 83 · raw

Data

How a connection went: one Sent per message tried, in order, then the error that ended it early (code 0 and "" when none did). A message after the error was not tried.

type Job source · line 87 · raw

Data

A message and its text, ready to send.

type Rc source · line 737 · raw

Data

A recipient and its RCPT line.

type Plan source · line 744 · raw

Data

How one message goes: with its commands sent together (PIPELINING, RFC 2920), with BDAT in place of DATA (CHUNKING, RFC 3030), with one final reply per recipient (LMTP, RFC 2033); the MAIL line, the recipients, the text.

type Proxy source · line 828 · raw

Data

A proxy: its kind (0: SOCKS5, RFC 1928; 1: HTTP CONNECT, RFC 9110 9.3.6), host and port, and a user and password ("" for none); no host means no proxy.

Definitions

def Out source · line 90 · raw

Type

def Batch.first.at source · line 93 · raw

@sent:List<&2, Sent> -> @x:Sent -> @code:U32 -> @error:String -> Batch

def Batch.first source · line 103 · raw

@x:Sent -> @b:Batch -> Batch

x as the first outcome when b has none: for a message already refused whose clean-up lost the connection, so the refusal is still reported.

def Batch.add source · line 107 · raw

@x:Sent -> @b:Batch -> Batch

def Job.mail source · line 111 · raw

@j:Job -> Mail

def Job.msg source · line 115 · raw

@j:Job -> String

def Caps.none source · line 119 · raw

Caps

def Mail.from source · line 125 · raw

@m:Mail -> String

def Smtp.uniq.at source · line 129 · raw

@seen:Bool -> @+x:String -> @rest:List<&2, String> -> List<&2, String>

def Smtp.uniq source · line 137 · raw

@xs:List<&2, String> -> @+seen:List<&2, String> -> List<&2, String>

def Mail.rcpts source · line 146 · raw

@m:Mail -> List<&2, String>

Who gets RCPT: To, Cc and Bcc, each address once.

def Mail.utf8 source · line 153 · raw

@m:Mail -> Bool

Whether the message needs SMTPUTF8 (RFC 6531): some local part is not ASCII. A non-ASCII domain alone does not: it goes as A-labels.

def Mail.ascii.at source · line 158 · raw

@utf8:Bool -> @m:Mail -> Mail

def Mail.ascii source · line 170 · raw

@+m:Mail -> Mail

The addresses as they will travel: with A-label domains, unless the message needs SMTPUTF8, which carries them as written.

def Mail.bad source · line 173 · raw

@+what:String -> @+a:String -> String

def Hdr.name.ok source · line 178 · raw

@s:String -> Bool

What is wrong with a message's addresses, or "" if nothing. A header name (RFC 5322 2.2): printable ASCII with no space or ":".

def Hdr.own source · line 186 · raw

List<&2, String>

The fields the client writes itself; an extra one would double them.

def Hdr.ok source · line 191 · raw

@+n:String -> Bool

def Hdr.bad source · line 196 · raw

@hs:List<&2, Hdr> -> String

The first header that cannot go, or "".

def Mail.files source · line 205 · raw

@m:Mail -> List<&2, 0x00e7af2de246c3a4c341d9ee49e68747/mime.Part>

def Mail.problem source · line 210 · raw

@+m:Mail -> String

What is wrong with a message's addresses and headers, or "".

def Cmd.text source · line 232 · raw

@raw:String -> String

A command's text with no CR or LF in it, whatever went in: the one place every command line passes through, so that no argument (an address, a name, a parameter) can end the line and start another command. LAWS.bend proves it for every string.

def Cmd.line source · line 236 · raw

@raw:String -> String

A command line: its text and the CRLF that ends it.

def Smtp.ehlo source · line 239 · raw

@name:String -> String

def Smtp.lhlo source · line 243 · raw

@name:String -> String

LMTP's greeting (RFC 2033 4.1).

def Smtp.helo source · line 246 · raw

@name:String -> String

def Smtp.size.param source · line 249 · raw

@+size:U32 -> @+len:U32 -> String

def Smtp.utf8.param source · line 254 · raw

@utf8:Bool -> @eight:Bool -> String

SMTPUTF8 (RFC 6531 3.4) and, when the server has it, BODY=8BITMIME: the headers then carry UTF-8 addresses (RFC 6532).

def Smtp.mail source · line 263 · raw

@from:String -> @+size:U32 -> @+len:U32 -> @utf8:Bool -> @eight:Bool -> @more:String -> String

MAIL, with the message's size (octets) when the server announced SIZE, and SMTPUTF8 when the message needs it; more: further parameters.

def Xtext.hex source · line 268 · raw

@+d:U32 -> Char

def Xtext.put source · line 271 · raw

@plain:Bool -> @+b:U32 -> @rest:String -> String

def Xtext.bytes source · line 278 · raw

@bs:List<&2, U32> -> String

def Xtext.of source · line 288 · raw

@s:String -> String

xtext (RFC 3461 4): printable ASCII but "+" and "=" as it is, any other byte as "+" and two hex digits.

def Dsn.ret source · line 294 · raw

@+ret:String -> String

RET's value (RFC 3461 4.3): FULL or HDRS, in any letter case; anything else is no parameter at all, so a value cannot carry a space (another parameter) or a line end (another command).

def Dsn.words source · line 298 · raw

@ws:List<&2, String> -> Bool

def Dsn.never source · line 306 · raw

@ws:List<&2, String> -> Bool

def Dsn.notify.ok source · line 315 · raw

@+ws:List<&2, String> -> Bool

Whether these are NOTIFY's values (RFC 3461 4.1): NEVER alone, or one or more of SUCCESS, FAILURE and DELAY.

def Dsn.notify.of source · line 318 · raw

@+ws:List<&2, String> -> String

def Dsn.notify source · line 323 · raw

@notify:String -> String

NOTIFY's parameter from a comma-separated list, in any letter case; nothing unless every value is one NOTIFY takes.

def Dsn.mail source · line 329 · raw

@ret:String -> @+envid:String -> String

The DSN parameters of MAIL (RFC 3461 4.3, 4.4): RET and ENVID, each only when asked for; ENVID goes as xtext, which has no space or line end.

def Dsn.rcpt.with source · line 333 · raw

@+n:String -> @+a:String -> String

def Dsn.rcpt source · line 340 · raw

@notify:String -> @a:String -> String

The DSN parameters of RCPT (RFC 3461 4.1, 4.2): NOTIFY and the original recipient (as xtext); a non-ASCII address gets no ORCPT (it would need RFC 6533's utf-8 form).

def Dsn.problem source · line 345 · raw

@+ret:String -> @+notify:String -> String

What is wrong with the DSN options, or "": a value that is given and is not one the parameter takes.

def Smtp.rcpt source · line 352 · raw

@to:String -> @more:String -> String

def Smtp.cram.resp source · line 357 · raw

@user:String -> @pass:String -> @challenge:String -> String

CRAM-MD5's response (RFC 2195): the user, a space, and the HMAC-MD5 of the server's challenge keyed by the password, in hex; all in base64.

def Sasl.soh source · line 364 · raw

String

def Smtp.plain.resp source · line 368 · raw

@user:String -> @pass:String -> String

AUTH PLAIN's response: base64 of NUL user NUL pass (RFC 4616).

def Smtp.plain source · line 373 · raw

@user:String -> @pass:String -> String

AUTH PLAIN with its initial response.

def Smtp.xoauth2.resp source · line 378 · raw

@user:String -> @token:String -> String

XOAUTH2's response (Google's and Microsoft's SASL XOAUTH2): "user=" user ^A "auth=Bearer " token ^A ^A, in base64.

def Sasl.name source · line 383 · raw

@s:String -> String

A saslname (RFC 5801 5.1): "=" as "=3D" and "," as "=2C".

def Smtp.bearer.resp source · line 396 · raw

@user:String -> @host:String -> @+port:U32 -> @token:String -> String

OAUTHBEARER's response (RFC 7628 3.1): the GS2 header with the user, then host, port and the bearer token, ^A-separated, in base64.

def Smtp.b64.line source · line 403 · raw

@s:String -> String

One AUTH LOGIN answer: the text in base64.

def Mech.has source · line 406 · raw

@ms:List<&2, String> -> @+w:String -> Bool

def Mech.only source · line 413 · raw

@+ms:List<&2, String> -> @+w:String -> @m:Mech -> Mech

def Mech.pick source · line 419 · raw

@a:Auth -> @oauth:Bool -> @+ms:List<&2, String> -> Mech

The mechanism for these options and these offers: the one asked for, if offered; else, for AAuto, XOAUTH2 or OAUTHBEARER with a token, and PLAIN, LOGIN or CRAM-MD5 (in this order, RFC 8314 4.1) without.

def Caps.size.of source · line 444 · raw

@m:Maybe<&2, U32> -> U32

def Caps.size source · line 451 · raw

@yes:Bool -> @l:String -> @old:U32 -> U32

def Caps.mechs source · line 458 · raw

@auth:Bool -> @l:String -> @old:List<&2, String> -> List<&2, String>

def Caps.line source · line 467 · raw

@+l:String -> @c:Caps -> Caps

One EHLO line, upper-cased, into the capabilities. "AUTH=" is the pre-standard spelling some servers still send.

def Caps.lines source · line 476 · raw

@ls:List<&2, String> -> @c:Caps -> Caps

def Caps.ext source · line 484 · raw

@c:Caps -> @w:String -> Bool

Whether the server announced this extension (its keyword, upper-case).

def Caps.of source · line 489 · raw

@text:String -> Caps

An EHLO reply's capabilities; its first line is the server's name.

def Smtp.data.end source · line 495 · raw

@nl:Bool -> String

def Smtp.stuff source · line 507 · raw

@s:String -> @bol:Bool -> @acc:String -> String

What follows DATA, in one pass: bare LFs become CRLFs, bare CRs go, a line that starts with "." gets a second one (so no line reads as the end), and the end line follows, after a CRLF if the text lacks one. bol: at the start of a line; acc: the output so far, reversed (a tail call per char, where a nested one would cost a frame per char).

def Smtp.data source · line 520 · raw

@msg:String -> String

def Hex.digit source · line 523 · raw

@+d:U32 -> Char

def Hex.go source · line 526 · raw

@n:Nat -> @+x:U32 -> @acc:String -> String

def Hex.of source · line 534 · raw

@x:U32 -> String

x as 8 hex digits.

def Hdr.value source · line 540 · raw

@+v:String -> String

An extra header's value: as written when it is printable ASCII (a structured value, like a URL in angle brackets, must not be encoded), else as RFC 2047 words.

def Hdr.shows source · line 543 · raw

@hs:List<&2, Hdr> -> String

def Smtp.header source · line 553 · raw

@name:String -> @+ms:List<&2, 0x00e7af2de246c3a4c341d9ee49e68747/addr.Mbox> -> String

A list header, or nothing when the list is empty.

def Smtp.nobody source · line 558 · raw

@+to:List<&2, 0x00e7af2de246c3a4c341d9ee49e68747/addr.Mbox> -> @+cc:List<&2, 0x00e7af2de246c3a4c341d9ee49e68747/addr.Mbox> -> String

With only Bcc recipients, To names an empty group (RFC 5322 A.1.3).

def Smtp.text source · line 565 · raw

@m:Mail -> @+t:U32 -> @id:String -> @+b:String -> String

The message (RFC 5322): Date, From, To, Cc, Reply-To, Subject, Message-ID, the extra headers, MIME-Version, then the MIME entity. Bcc is never written. t is the time, id a unique string, b the boundary stem.

def Opts.host source · line 581 · raw

@o:Opts -> String

def Opts.port source · line 585 · raw

@o:Opts -> U32

def Opts.mode source · line 589 · raw

@o:Opts -> Mode

def Opts.cafile source · line 593 · raw

@o:Opts -> String

def Opts.helo source · line 597 · raw

@o:Opts -> String

def Opts.user source · line 601 · raw

@o:Opts -> String

def Opts.pass source · line 605 · raw

@o:Opts -> String

def Opts.auth source · line 609 · raw

@o:Opts -> Auth

def Opts.token source · line 613 · raw

@o:Opts -> String

def Opts.debug source · line 617 · raw

@o:Opts -> Bool

def Kv.get source · line 621 · raw

@kvs:List<&2, Kv> -> @+k:String -> @found:String -> String

def Opts.opt source · line 630 · raw

@o:Opts -> @k:String -> String

A named option's value, or "".

def Opts.on source · line 634 · raw

@o:Opts -> @k:String -> Bool

def Opts.new source · line 647 · raw

@host:String -> Opts

The usual submission: STARTTLS on port 587, the system's trust store, no AUTH yet.

def Opts.via source · line 652 · raw

@o:Opts -> @port:U32 -> @mode:Mode -> Opts

o with this port and mode (Tls{} for implicit TLS on 465, Plain{} for none).

def Opts.login source · line 658 · raw

@o:Opts -> @user:String -> @pass:String -> Opts

o with a user and password; the mechanism is picked from the server's offers.

def Opts.oauth source · line 663 · raw

@o:Opts -> @user:String -> @token:String -> Opts

o with a user and an OAuth 2 access token (XOAUTH2 or OAUTHBEARER).

def Opts.with source · line 668 · raw

@o:Opts -> @key:String -> @val:String -> Opts

o with a named option set (see Opts): Opts.with(o, "proxy", "socks5://...").

def Mail.new source · line 675 · raw

@from:String -> @to:String -> @subject:String -> @text:String -> Mail

A plain-text message; from is one mailbox and to a list, both as people write them ("Name <addr>", comma-separated).

def Mail.html source · line 680 · raw

@m:Mail -> @html:String -> Mail

m with an HTML alternative to its text.

def Mail.copy source · line 685 · raw

@m:Mail -> @cc:String -> @bcc:String -> Mail

m with these Cc and Bcc lists (as people write them).

def Mail.attach source · line 692 · raw

@m:Mail -> @+name:String -> @data:List<&2, U32> -> Mail

m with one more attachment: a file name, its bytes, and a type guessed from the name.

def Mail.header source · line 698 · raw

@m:Mail -> @name:String -> @value:String -> Mail

m with one more header field.

def Smtp.wait source · line 708 · raw

U32

Timeouts in ms (RFC 5321 4.5.3.2): 5 min for the greeting, MAIL, RCPT and the rest; 2 min for DATA; 10 min for the end of the message.

def Smtp.wait.data source · line 711 · raw

U32

def Smtp.wait.end source · line 714 · raw

U32

def Smtp.fuel source · line 718 · raw

Nat

Reads per reply before giving up (each waits up to its timeout).

def Smtp.fail source · line 722 · raw

@code:U32 -> @msg:String -> Out

The connection ended early, with nothing more sent.

def Smtp.octets.go source · line 725 · raw

@s:String -> @+n:U32 -> U32

def Smtp.octets source · line 733 · raw

@s:String -> U32

The UTF-8 size of s, counted without building the bytes.

def Plan.piped source · line 748 · raw

@p:Plan -> Bool

def Plan.bdat source · line 752 · raw

@p:Plan -> Bool

def Plan.lmtp source · line 756 · raw

@p:Plan -> Bool

def Plan.mail source · line 760 · raw

@p:Plan -> String

def Plan.rcpts source · line 764 · raw

@p:Plan -> List<&2, Rc>

def Plan.msg source · line 768 · raw

@p:Plan -> String

def Plan.rcs source · line 772 · raw

@to:List<&2, String> -> @+notify:String -> List<&2, Rc>

def Plan.lines source · line 779 · raw

@rs:List<&2, Rc> -> String

def Plan.blob source · line 789 · raw

@+p:Plan -> String

Everything up to the message in one write: MAIL, every RCPT, and DATA unless BDAT follows.

def Plan.chunk source · line 795 · raw

@+p:Plan -> String

The message as one last chunk (RFC 3030 2): its exact size, then its bytes, with no dot-stuffing and no end line.

def Smtp.plan source · line 801 · raw

@+o:Opts -> @+c:Caps -> @+j:Job -> Plan

The plan for a message, from what was asked and what the server offers: an extension is used only when announced, and the DSN parameters only with DSN.

def Smtp.note source · line 812 · raw

@+r:0x00e7af2de246c3a4c341d9ee49e68747/reply.Reply -> @a:String -> String

def Sasl.cancel source · line 821 · raw

@+m:String -> String

What ends an exchange the server wants to go on with after the last response (a 334 carrying an error): an empty line for XOAUTH2, ^A for OAUTHBEARER (RFC 7628 3.2.3), "*" otherwise (RFC 4954 4).

def Proxy.none source · line 831 · raw

Proxy

def Proxy.port source · line 834 · raw

@m:Maybe<&2, U32> -> @dflt:U32 -> U32

def Proxy.host source · line 842 · raw

@hp:List<&2, String> -> @kind:U32 -> @dflt:U32 -> @user:String -> @pass:String -> Proxy

"host[:port]"; dflt: the port when absent.

def Proxy.creds source · line 853 · raw

@up:List<&2, String> -> @hp:List<&2, String> -> @kind:U32 -> @dflt:U32 -> Proxy

"user[:pass]".

def Proxy.parts source · line 864 · raw

@+ps:List<&2, String> -> @kind:U32 -> @dflt:U32 -> Proxy

"[user[:pass]@]host[:port]"; the last "@" divides them.

def Proxy.rest source · line 869 · raw

@+rest:String -> @kind:U32 -> @dflt:U32 -> Proxy

def Proxy.of source · line 876 · raw

@+url:String -> Proxy

A proxy from "socks5://[user[:pass]@]host[:port]" (port 1080; also "socks5h://", the same here: the proxy always resolves the name) or "http://..." (port 8080); "" and anything else give none.

def Proxy.named source · line 884 · raw

@p:Proxy -> Bool

def Proxy.problem source · line 890 · raw

@+url:String -> String

A proxy that was asked for but cannot be read is an error: going on without it would connect directly, which is what it was there to avoid.

def Opts.problem source · line 896 · raw

@+o:Opts -> String

What is wrong with the named options, or "": a proxy that cannot be read, or a DSN value its parameter does not take.

def Smtp.job source · line 901 · raw

@+m:Mail -> @+t:U32 -> @+a:U32 -> @b:U32 -> @c:U32 -> Job

def Canon.header source · line 907 · raw

@+c:String -> Bool

The header form of "header[/body]" (c=, RFC 6376 3.5): relaxed unless it says simple.

def Canon.body source · line 912 · raw

@+c:String -> Bool

The body form: relaxed by default here; with only the header form given, simple, as the tag reads.

def Smtp.problems source · line 916 · raw

@ms:List<&2, Mail> -> String

The first message's problem, or "" (and "no messages" for none).

def Smtp.asciis source · line 924 · raw

@ms:List<&2, Mail> -> List<&2, Mail>