Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

mail.sasl

import mail

mail exposes this as mail.sasl, so import mail is enough and the names are called as mail.sasl.*. import mail.sasl reaches 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

mechanismwhat it sends
PLAINthe password, so it needs TLS under it
LOGINthe same, one prompt at a time, for servers that only do this
CRAM-MD5a keyed digest of the server’s challenge
SCRAM-SHA-1, SCRAM-SHA-256a proof the server checks without the password, and one back
XOAUTH2, OAUTHBEARERa bearer token, which is what the large providers want
EXTERNALnothing: the client certificate already said who this is
ANONYMOUSnothing, 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.

credentialused by
username, passwordPLAIN, LOGIN, CRAM-MD5, SCRAM-*
username, tokenXOAUTH2, OAUTHBEARER
authorize_asPLAIN, EXTERNAL, SCRAM-*
host, portOAUTHBEARER
traceANONYMOUS

Parameters

  • name (string) — One of PREFERENCE.
  • 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(), so echo and print() 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(), so echo and print() 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(), so echo and print() 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(), so echo and print() 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(), so echo and print() 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 the Bearer word.

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(), so echo and print() 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(), so echo and print() 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(), so echo and print() 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(), so echo and print() 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) — sha256 when not given, or sha1.
  • 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