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.dkim

import mail

mail exposes this as mail.dkim, so import mail is enough and the names are called as mail.dkim.*. import mail.dkim reaches the same definitions directly.

DomainKeys Identified Mail: signing outgoing messages so a receiver can tell they came from the domain they claim to, and checking the signatures on incoming ones.

A signature covers the body and a chosen set of headers. The public key lives in the domain’s own DNS, under the selector the signature names, which is what ties the message to the domain.

import mail
import mail.dkim { Signer }

var signer = Signer('example.com', 'default', private_key)

signer.sign(note)

mail.send('smtp://mail.example.com', note)

Verification looks the key up through net.resolver unless handed something else to look it up with:

import mail.dkim

for result in dkim.verify(incoming) {
  echo '${result.domain}: ${result.valid ? 'ok' : result.reason}'
}

What is covered

Both canonicalisations, simple and relaxed, on headers and on bodies, and both signing algorithms in use: rsa-sha256 and the ed25519-sha256 of RFC 8463.

A signature says nothing about who wrote the message, only that the domain that signed it takes responsibility for it. Deciding what a valid signature from a given domain is worth is a policy question, and the answer is usually DMARC.

Constants

ALGORITHMS

mail.dkim.ALGORITHMS: list = [...]

The signing algorithms this implements.

CANONICALISATIONS

mail.dkim.CANONICALISATIONS: list = [...]

The canonicalisations this implements, on either headers or body.

simple covers the bytes as they are, which means any change at all on the way breaks the signature. relaxed forgives the whitespace and folding changes a mail server may make in passing, and is what almost everything uses.

DEFAULT_HEADERS

mail.dkim.DEFAULT_HEADERS: list = [...]

The headers signed when the signer is not told which to sign.

A header that is not in the message is skipped rather than signed as empty, and one that is listed twice is signed twice, which is how a signature says “there was exactly one of these”.

Functions

canonicalise_body()

mail.dkim.canonicalise_body(raw, method: string) -> bytes

Canonicalises a body.

Parameters

  • raw (string|bytes)
  • method (string) — simple or relaxed.

Returns bytes

Raises DkimError if method is neither.

canonicalise_header()

mail.dkim.canonicalise_header(name: string, value: string, method: string)

Canonicalises one header.

Parameters

  • name (string)
  • value (string) — The value as it was written, folding and all.
  • method (string) — simple or relaxed.

Returns — string, without the line break that ends it.

Raises DkimError if method is neither.

public_key_pem()

mail.dkim.public_key_pem(encoded: string, algorithm: string) -> string

Turns the p= value of a key record into a PEM the crypto module will accept.

An RSA key is published as the whole SubjectPublicKeyInfo structure and only needs wrapping. An Ed25519 key is published as the 32 raw bytes, and has that structure put around it here.

Parameters

  • encoded (string) — The base64 from the record’s p= tag.
  • algorithm (string) — One of ALGORITHMS.

Returns string

Raises DkimError if the key will not decode.

verify()

mail.dkim.verify(message, lookup) -> list

Checks every signature on a message.

One result comes back per DKIM-Signature header, in the order they appear, whether it passed or not. A message with no signatures gives an empty list, which is not a failure: it is a message nobody signed.

import mail.dkim

for result in dkim.verify(incoming) {
  echo '${result.domain}: ${result.valid ? 'pass' : result.reason}'
}

Parameters

  • message (Message)
  • lookup (?function) — Given a name, returns the text records published under it. net.resolver is used when not given.

Returns list — of Result

Note: A message that was parsed is checked against the bytes it was parsed from, which is the only thing a signature can be checked against. Changing a parsed message and checking it again reports on the message that arrived, not the one now in hand.

is_signed()

mail.dkim.is_signed(message, lookup) -> bool

Whether a message carries at least one signature that checks out.

Parameters

  • message (Message)
  • lookup (?function)

Returns bool

Classes

DkimError

class mail.dkim.DkimError < MailError

Raised when a signature cannot be built: an algorithm this does not implement, a key that will not parse, a canonicalisation that is not one of the two.

  • printable — has a @to_string(), so echo and print() show something useful

DkimError.to_string()

mail.dkim.DkimError.to_string()

Signature

class mail.dkim.Signature

One DKIM-Signature header, read into its parts.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Signature(tags: dict, raw: ?string)

Parameters

  • tags (dict) — The tags of the header, by name.
  • raw (?string) — The header value it was read from.

Signature.parse()

mail.dkim.Signature.parse(value: string) -> Signature

Reads a DKIM-Signature header value.

Parameters

  • value (string)

Returns Signature

Signature.record_name()

mail.dkim.Signature.record_name() -> string

The name the public key is published under.

Returns string

Signature.to_string()

mail.dkim.Signature.to_string()

Signer

class mail.dkim.Signer

Signs outgoing messages for one domain with one key.

Build one and keep it: a signer holds no connection and no state beyond its key, and signing is the only thing it does.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Signer(domain: string, selector: string, private_key: string, options: ?dict)
optiondefaultwhat it does
algorithmrsa-sha256or ed25519-sha256
canonicalisationrelaxed/relaxedheaders then body
headersDEFAULT_HEADERSwhich headers to cover
identitynonethe i= tag, an address within the domain
expires_innoneseconds until the signature stops counting
timestamptruewhether to record when it was signed

Parameters

  • domain (string) — The domain taking responsibility.
  • selector (string) — Which of the domain’s keys this is.
  • private_key (string) — The key, PEM encoded.
  • options (?dict)

Raises DkimError if an option names something not implemented.

Signer.sign()

mail.dkim.Signer.sign(message) -> string

Signs a message, adding the DKIM-Signature header to the top of it.

The message is signed as it stands, so everything else about it has to be settled first. Changing a signed header afterwards breaks the signature.

Parameters

  • message (Message)

Returns string — the header value that was added.

Raises DkimError if the key will not sign.

Signer.to_string()

mail.dkim.Signer.to_string()

Result

class mail.dkim.Result

What checking one signature came to.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Result(signature, valid: bool, reason: ?string)

Parameters

  • signature (Signature) — The signature that was checked.
  • valid (bool)
  • reason (?string) — Why not, when it is not valid.

Result.to_string()

mail.dkim.Result.to_string()

2026, Richard Ore and Zuri contributors