core.bend checks
raw source on the hub · import 0xfd64aefef43747d3eaeea50c10a112e5/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".
Hdr@name:String -> @value:String -> Hdr
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.
Mail@from:0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox -> @to:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> @cc:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> @bcc:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> @reply:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> @subject:String -> @text:String -> @html:String -> @files:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/mime.Part> -> @headers:List<&2, Hdr> -> Mail
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).
PlainMode
TlsMode
StartTlsMode
type Auth source · line 34 · raw
Data
The AUTH mechanism asked for; AAuto picks from what the server offers.
AAutoAuth
APlainAuth
ALoginAuth
ACramAuth
AXoauth2Auth
ABearerAuth
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".
Kv@key:String -> @val:String -> Kv
type Opts source · line 54 · raw
Data
Opts@host:String -> @port:U32 -> @mode:Mode -> @cafile:String -> @helo:String -> @user:String -> @pass:String -> @auth:Auth -> @token:String -> @debug:Bool -> @more:List<&2, Kv> -> Opts
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).
Caps@tls:Bool -> @mechs:List<&2, String> -> @size:U32 -> @utf8:Bool -> @eight:Bool -> @exts:List<&2, String> -> Caps
type Mech source · line 66 · raw
Data
The mechanism to use, or why there is none.
MPlainMech
MLoginMech
MCramMech
MXoauth2Mech
MBearerMech
MNo@why:String -> Mech
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.
Sent@code:U32 -> @error:String -> @refused:List<&2, String> -> Sent
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.
Batch@sent:List<&2, Sent> -> @code:U32 -> @error:String -> Batch
type Job source · line 87 · raw
Data
A message and its text, ready to send.
Job@mail:Mail -> @msg:String -> Job
type Rc source · line 672 · raw
Data
A recipient and its RCPT line.
Rc@addr:String -> @line:String -> Rc
type Plan source · line 679 · 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.
Plan@piped:Bool -> @bdat:Bool -> @lmtp:Bool -> @mail:String -> @rcpts:List<&2, Rc> -> @msg:String -> Plan
type Proxy source · line 763 · 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.
Proxy@kind:U32 -> @host:String -> @port:U32 -> @user:String -> @pass:String -> 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.problem source · line 206 · raw
@+m:Mail -> String
What is wrong with a message's addresses and headers, or "".
def Smtp.ehlo source · line 221 · raw
@name:String -> String
def Smtp.lhlo source · line 225 · raw
@name:String -> String
LMTP's greeting (RFC 2033 4.1).
def Smtp.helo source · line 228 · raw
@name:String -> String
def Smtp.size.param source · line 231 · raw
@+size:U32 -> @+len:U32 -> String
def Smtp.utf8.param source · line 236 · 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 245 · 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 250 · raw
@+d:U32 -> Char
def Xtext.put source · line 253 · raw
@plain:Bool -> @+b:U32 -> @rest:String -> String
def Xtext.bytes source · line 260 · raw
@bs:List<&2, U32> -> String
def Xtext.of source · line 270 · 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.mail source · line 275 · 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.
def Dsn.rcpt source · line 282 · raw
@+notify:String -> @+a:String -> String
The DSN parameters of RCPT (RFC 3461 4.1, 4.2): NOTIFY and the original recipient; a non-ASCII address gets no ORCPT (it would need RFC 6533's utf-8 form).
def Smtp.rcpt source · line 287 · raw
@to:String -> @more:String -> String
def Smtp.cram.resp source · line 292 · 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 299 · raw
String
def Smtp.plain.resp source · line 303 · raw
@user:String -> @pass:String -> String
AUTH PLAIN's response: base64 of NUL user NUL pass (RFC 4616).
def Smtp.plain source · line 308 · raw
@user:String -> @pass:String -> String
AUTH PLAIN with its initial response.
def Smtp.xoauth2.resp source · line 313 · 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 318 · raw
@s:String -> String
A saslname (RFC 5801 5.1): "=" as "=3D" and "," as "=2C".
def Smtp.bearer.resp source · line 331 · 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 338 · raw
@s:String -> String
One AUTH LOGIN answer: the text in base64.
def Mech.has source · line 341 · raw
@ms:List<&2, String> -> @+w:String -> Bool
def Mech.only source · line 348 · raw
@+ms:List<&2, String> -> @+w:String -> @m:Mech -> Mech
def Mech.pick source · line 354 · 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 379 · raw
@m:Maybe<&2, U32> -> U32
def Caps.size source · line 386 · raw
@yes:Bool -> @l:String -> @old:U32 -> U32
def Caps.mechs source · line 393 · raw
@auth:Bool -> @l:String -> @old:List<&2, String> -> List<&2, String>
def Caps.line source · line 402 · 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 411 · raw
@ls:List<&2, String> -> @c:Caps -> Caps
def Caps.ext source · line 419 · raw
@c:Caps -> @w:String -> Bool
Whether the server announced this extension (its keyword, upper-case).
def Caps.of source · line 424 · raw
@text:String -> Caps
An EHLO reply's capabilities; its first line is the server's name.
def Smtp.data.end source · line 430 · raw
@nl:Bool -> String
def Smtp.stuff source · line 442 · 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 455 · raw
@msg:String -> String
def Hex.digit source · line 458 · raw
@+d:U32 -> Char
def Hex.go source · line 461 · raw
@n:Nat -> @+x:U32 -> @acc:String -> String
def Hex.of source · line 469 · raw
@x:U32 -> String
x as 8 hex digits.
def Hdr.value source · line 475 · 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 478 · raw
@hs:List<&2, Hdr> -> String
def Smtp.header source · line 488 · raw
@name:String -> @+ms:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> String
A list header, or nothing when the list is empty.
def Smtp.nobody source · line 493 · raw
@+to:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> @+cc:List<&2, 0xfd64aefef43747d3eaeea50c10a112e5/addr.Mbox> -> String
With only Bcc recipients, To names an empty group (RFC 5322 A.1.3).
def Smtp.text source · line 500 · 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 516 · raw
@o:Opts -> String
def Opts.port source · line 520 · raw
@o:Opts -> U32
def Opts.mode source · line 524 · raw
@o:Opts -> Mode
def Opts.cafile source · line 528 · raw
@o:Opts -> String
def Opts.helo source · line 532 · raw
@o:Opts -> String
def Opts.user source · line 536 · raw
@o:Opts -> String
def Opts.pass source · line 540 · raw
@o:Opts -> String
def Opts.auth source · line 544 · raw
@o:Opts -> Auth
def Opts.token source · line 548 · raw
@o:Opts -> String
def Opts.debug source · line 552 · raw
@o:Opts -> Bool
def Kv.get source · line 556 · raw
@kvs:List<&2, Kv> -> @+k:String -> @found:String -> String
def Opts.opt source · line 565 · raw
@o:Opts -> @k:String -> String
A named option's value, or "".
def Opts.on source · line 569 · raw
@o:Opts -> @k:String -> Bool
def Opts.new source · line 582 · raw
@host:String -> Opts
The usual submission: STARTTLS on port 587, the system's trust store, no AUTH yet.
def Opts.via source · line 587 · 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 593 · 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 598 · 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 603 · 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 610 · 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 615 · raw
@m:Mail -> @html:String -> Mail
m with an HTML alternative to its text.
def Mail.copy source · line 620 · raw
@m:Mail -> @cc:String -> @bcc:String -> Mail
m with these Cc and Bcc lists (as people write them).
def Mail.attach source · line 627 · 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 633 · raw
@m:Mail -> @name:String -> @value:String -> Mail
m with one more header field.
def Smtp.wait source · line 643 · 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 646 · raw
U32
def Smtp.wait.end source · line 649 · raw
U32
def Smtp.fuel source · line 653 · raw
Nat
Reads per reply before giving up (each waits up to its timeout).
def Smtp.fail source · line 657 · raw
@code:U32 -> @msg:String -> Out
The connection ended early, with nothing more sent.
def Smtp.octets.go source · line 660 · raw
@s:String -> @+n:U32 -> U32
def Smtp.octets source · line 668 · raw
@s:String -> U32
The UTF-8 size of s, counted without building the bytes.
def Plan.piped source · line 683 · raw
@p:Plan -> Bool
def Plan.bdat source · line 687 · raw
@p:Plan -> Bool
def Plan.lmtp source · line 691 · raw
@p:Plan -> Bool
def Plan.mail source · line 695 · raw
@p:Plan -> String
def Plan.rcpts source · line 699 · raw
@p:Plan -> List<&2, Rc>
def Plan.msg source · line 703 · raw
@p:Plan -> String
def Plan.rcs source · line 707 · raw
@to:List<&2, String> -> @+notify:String -> List<&2, Rc>
def Plan.lines source · line 714 · raw
@rs:List<&2, Rc> -> String
def Plan.blob source · line 724 · 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 730 · 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 736 · 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 747 · raw
@+r:0xfd64aefef43747d3eaeea50c10a112e5/reply.Reply -> @a:String -> String
def Sasl.cancel source · line 756 · 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 766 · raw
Proxy
def Proxy.port source · line 769 · raw
@m:Maybe<&2, U32> -> @dflt:U32 -> U32
def Proxy.host source · line 777 · 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 788 · raw
@up:List<&2, String> -> @hp:List<&2, String> -> @kind:U32 -> @dflt:U32 -> Proxy
"user[:pass]".
def Proxy.parts source · line 799 · raw
@+ps:List<&2, String> -> @kind:U32 -> @dflt:U32 -> Proxy
"[user[:pass]@]host[:port]"; the last "@" divides them.
def Proxy.rest source · line 804 · raw
@+rest:String -> @kind:U32 -> @dflt:U32 -> Proxy
def Proxy.of source · line 811 · 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 819 · raw
@p:Proxy -> Bool
def Proxy.problem source · line 825 · 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 Smtp.job source · line 829 · raw
@+m:Mail -> @+t:U32 -> @+a:U32 -> @b:U32 -> @c:U32 -> Job
def Canon.header source · line 835 · 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 840 · 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 844 · raw
@ms:List<&2, Mail> -> String
The first message's problem, or "" (and "no messages" for none).
def Smtp.asciis source · line 852 · raw
@ms:List<&2, Mail> -> List<&2, Mail>