mail.sasl
import mail
mail.sasl, soimport mailis enough and the names are called asmail.sasl.*.import mail.saslreaches the same definitions directly.
The authentication mechanisms all three mail protocols share.
SMTP, IMAP and POP3 each have their own way of starting an authentication exchange and their own way of carrying the bytes, but what travels inside is the same in all three. This module holds the mechanisms themselves, so none of the protocols has to.
import mail.sasl
var mechanism = sasl.of('PLAIN', { username: 'ann', password: 'secret' })
echo mechanism.name()
echo mechanism.start().to_string().replace('/\0/', '|')
PLAIN
|ann|secret
What is here
| mechanism | what it sends |
|---|---|
PLAIN | the password, so it needs TLS under it |
LOGIN | the same, one prompt at a time, for servers that only do this |
CRAM-MD5 | a keyed digest of the server’s challenge |
SCRAM-SHA-1, SCRAM-SHA-256 | a proof the server checks without the password, and one back |
XOAUTH2, OAUTHBEARER | a bearer token, which is what the large providers want |
EXTERNAL | nothing: the client certificate already said who this is |
ANONYMOUS | nothing, on purpose |
The clients pick the strongest mechanism both ends offer, in the order
PREFERENCE gives.
Constants
PREFERENCE
mail.sasl.PREFERENCE: list = [...]
The mechanisms this implements, strongest first, which is the order a client picks from what a server offers.
SCRAM proves the password without sending it and proves the server
knew it too. CRAM-MD5 proves it without sending it but proves nothing
about the server, and its digest is old. The rest send something worth
protecting, which is why every client here refuses to use them without
TLS.
NEEDS_TLS
mail.sasl.NEEDS_TLS: list = [...]
The mechanisms that put the password itself on the wire, and so must never be used without TLS.
Functions
prepare()
mail.sasl.prepare(text: string) -> string
Prepares a username or password the way RFC 4013 says to, so that two spellings of the same characters authenticate the same.
The space mappings, the deletions and the prohibited characters are all applied. Unicode normalisation is not, so text that needs it must arrive already normalised; for anything typed on a keyboard in any Latin script this makes no difference at all.
Parameters
text(string)
Returns string
Raises AuthenticationError if the text holds a character that may
not appear in a credential.
response()
mail.sasl.response(username: string, password: string, challenge) -> string
The answer to a CRAM-MD5 challenge.
Used by the mechanism, and by a server checking one, which has to work out the same answer to compare against.
Parameters
username(string)password(string)challenge(string|bytes)
Returns string
of()
mail.sasl.of(name: string, credentials: dict) -> Mechanism
Builds a mechanism by name.
| credential | used by |
|---|---|
username, password | PLAIN, LOGIN, CRAM-MD5, SCRAM-* |
username, token | XOAUTH2, OAUTHBEARER |
authorize_as | PLAIN, EXTERNAL, SCRAM-* |
host, port | OAUTHBEARER |
trace | ANONYMOUS |
Parameters
name(string) — One ofPREFERENCE.credentials(dict)
Returns Mechanism
Raises AuthenticationError if the name is not one of them, or a
credential it needs is missing.
choose()
mail.sasl.choose(offered: list, credentials: dict, secure: bool, allowed: ?list) -> string|nil
Picks the mechanism to use.
Takes what the server offers and what the caller can supply, and returns
the strongest name both sides have, or nil when there is none.
Mechanisms that put the password on the wire are left out entirely when the connection is not encrypted, which is why a client that refuses to authenticate in the clear does not have to check for it separately.
import mail.sasl
echo sasl.choose(['PLAIN', 'LOGIN', 'CRAM-MD5'], { password: 'x' }, false)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, true)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, false)
CRAM-MD5
PLAIN
nil
Parameters
offered(list) — The names the server advertised.credentials(dict) — What the caller has to offer.secure(bool) — Whether the connection is encrypted.allowed(?list) — Restricts the choice to these names.
Returns string|nil
Classes
Mechanism
class mail.sasl.Mechanism
What every mechanism looks like from the outside.
A client calls start() once, sends whatever comes back if anything
does, and then calls step() with each challenge the server sends until
is_done().
- printable — has a
@to_string(), soechoandprint()show something useful
Mechanism.name()
mail.sasl.Mechanism.name() -> string
The name to offer the server.
Returns string
Mechanism.start()
mail.sasl.Mechanism.start() -> bytes|nil
The response to send before the server has said anything, or nil for a
mechanism that waits to be asked.
Returns bytes|nil
Mechanism.step()
mail.sasl.Mechanism.step(challenge) -> bytes|nil
The response to one challenge.
Parameters
challenge(bytes) — The server’s challenge, already decoded.
Returns bytes|nil
Mechanism.is_done()
mail.sasl.Mechanism.is_done() -> bool
Whether the exchange is over as far as this mechanism is concerned. A server may still have the last word.
Returns bool
Mechanism.to_string()
mail.sasl.Mechanism.to_string()
Plain
class mail.sasl.Plain < Mechanism
PLAIN: the username and the password, separated by zero bytes.
Everything worth having is on the wire, so this belongs under TLS and nowhere else.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Plain(username: string, password: string, authorize_as: ?string)
Parameters
username(string)password(string)authorize_as(?string) — The account to act as, when it is not the one being authenticated.
Plain.name()
mail.sasl.Plain.name()
Plain.start()
mail.sasl.Plain.start()
Login
class mail.sasl.Login < Mechanism
LOGIN: the username and the password again, one prompt at a time.
Never standardised and entirely superseded by PLAIN, but some servers
offer nothing else.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Login(username: string, password: string)
Parameters
username(string)password(string)
Login.name()
mail.sasl.Login.name()
Login.step()
mail.sasl.Login.step(challenge)
Login.is_done()
mail.sasl.Login.is_done()
CramMd5
class mail.sasl.CramMd5 < Mechanism
CRAM-MD5: a keyed digest of the server’s challenge, so the password
never travels.
The digest is old and the exchange proves nothing about the server, so prefer SCRAM where there is a choice.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.CramMd5(username: string, password: string)
Parameters
username(string)password(string)
CramMd5.name()
mail.sasl.CramMd5.name()
CramMd5.step()
mail.sasl.CramMd5.step(challenge)
CramMd5.is_done()
mail.sasl.CramMd5.is_done()
XOAuth2
class mail.sasl.XOAuth2 < Mechanism
XOAUTH2: a bearer token rather than a password, which is what Google
and Microsoft accept.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.XOAuth2(username: string, token: string)
Parameters
username(string) — The account the token is for.token(string) — The access token, without theBearerword.
XOAuth2.name()
mail.sasl.XOAuth2.name()
XOAuth2.start()
mail.sasl.XOAuth2.start()
XOAuth2.step()
mail.sasl.XOAuth2.step(challenge)
OAuthBearer
class mail.sasl.OAuthBearer < Mechanism
OAUTHBEARER: the standardised form of the same idea, from RFC 7628.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.OAuthBearer(username: string, token: string, host: ?string, port: ?number)
Parameters
username(string)token(string)host(?string) — The server being reached, which the token may be bound to.port(?number)
OAuthBearer.name()
mail.sasl.OAuthBearer.name()
OAuthBearer.start()
mail.sasl.OAuthBearer.start()
OAuthBearer.step()
mail.sasl.OAuthBearer.step(challenge)
External
class mail.sasl.External < Mechanism
EXTERNAL: the connection already established who this is, usually with
a client certificate.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.External(authorize_as: ?string)
Parameters
authorize_as(?string) — The account to act as. Empty means whichever one the certificate names.
External.name()
mail.sasl.External.name()
External.start()
mail.sasl.External.start()
Anonymous
class mail.sasl.Anonymous < Mechanism
ANONYMOUS: no identity at all, with an optional note saying who is
knocking.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Anonymous(trace: ?string)
Parameters
trace(?string) — An address or a token, for the server’s log.
Anonymous.name()
mail.sasl.Anonymous.name()
Anonymous.start()
mail.sasl.Anonymous.start()
Scram
class mail.sasl.Scram < Mechanism
SCRAM-SHA-1 and SCRAM-SHA-256: the password is never sent, the
server never has to store it, and the server proves it knew it too.
This is the mechanism to use wherever a server offers it. Channel
binding, the -PLUS form, is not offered: it needs a value out of the
TLS session that nothing here exposes.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Scram(username: string, password: string, digest: ?string, authorize_as: ?string, nonce: ?string)
Parameters
username(string)password(string)digest(?string) —sha256when not given, orsha1.authorize_as(?string)nonce(?string) — The client nonce. A fresh random one is generated when not given, which is what every real exchange wants; passing one is for reproducing a known exchange.
Raises AuthenticationError if digest is neither.
Scram.name()
mail.sasl.Scram.name()
Scram.start()
mail.sasl.Scram.start()
Scram.step()
mail.sasl.Scram.step(challenge)
Scram.is_done()
mail.sasl.Scram.is_done()
2026, Richard Ore and Zuri contributors