API

mailsuite.imap

class mailsuite.imap.IMAPClient(host: str, username: str | None = None, password: str | None = None, port: int = 993, ssl: bool = True, ssl_context: SSLContext | None = None, verify: bool = True, timeout: int = 30, max_retries: int = 4, initial_folder: str = 'INBOX', idle_callback=None, idle_timeout: int = 30, oauth2_token: str | None = None, oauth2_token_provider: Callable[[], str] | None = None, oauth2_mechanism: str = 'XOAUTH2', oauth2_vendor: str | None = None, config_reloading: Callable[[], bool] | None = None)[source]

A simplified IMAP client

create_folder(folder: str, _attempt: int = 1)[source]

Creates an IMAP folder at the given path

Parameters:
  • folder – The path of the folder to create

  • _attempt – The attempt number

delete_messages(messages: list[int] | list[str] | str | int, silent: bool = True, _attempt: int = 1)[source]

Deletes the given messages by Message UIDs

Parameters:
  • messages – A list of UIDs of messages to delete

  • silent – Do it silently

  • _attempt – The attempt number

fetch_message(msg_uid: int, parse: bool = False, _attempt: int = 1) str | dict[source]

Fetch a message by UID, and optionally parse it

Parameters:
  • msg_uid – The message UID

  • parse – Return parsed results from mailparser

  • _attempt – The attempt number

Returns:

The raw mail message, including headers dict: A parsed email message

Return type:

str

move_messages(msg_uids: int | list[int], folder_path: str, _attempt: int = 1)[source]

Move the emails with the given UIDs to the given folder

Parameters:
  • msg_uids – A UID or list of UIDs of messages to move

  • folder_path – The path of the destination folder

  • _attempt – The attempt number

reset_connection()[source]

Resets the connection to the IMAP server

exception mailsuite.imap.MaxRetriesExceeded[source]

Raised when the maximum number of retries is exceeded

mailsuite.smtp

exception mailsuite.smtp.SMTPError[source]

Raised when a SMTP error occurs

mailsuite.smtp.send_email(host: str, message_from: str, message_to: list[str] | None = None, message_cc: list | None = None, message_bcc: list | None = None, port: int = 0, require_encryption: bool = False, verify: bool = True, username: str | None = None, password: str | None = None, oauth2_token: str | None = None, oauth2_token_provider: Callable[[], str] | None = None, oauth2_mechanism: str = 'XOAUTH2', oauth2_vendor: str | None = None, envelope_from: str | None = None, subject: str | None = None, message_headers: dict | None = None, attachments: list[tuple[str, bytes]] | None = None, plain_message: str | None = None, html_message: str | None = None, dkim_private_key: str | None = None, dkim_selector: str | None = None, dkim_domain: str | None = None, dkim_additional_headers: list[str] | None = None)[source]

Send an email using a SMTP relay

Parameters:
  • host – Mail server hostname or IP address

  • message_from – The value of the message “From” header

  • message_to – A list of addresses to send mail to

  • message_cc – A list of addresses to Carbon Copy (CC)

  • message_bcc – A list of addresses to Blind Carbon Copy (BCC)

  • port – Port to use

  • require_encryption – Require a SSL/TLS connection from the start

  • verify – Verify the SSL/TLS certificate

  • username – An optional username

  • password – An optional password (omit when using OAuth2)

  • oauth2_token – A static OAuth2 access token. Provide this (or oauth2_token_provider) together with username to authenticate with OAuth2 instead of a password.

  • oauth2_token_provider – A zero-arg callable returning a current OAuth2 access token, invoked at send time so a fresh token is used. Takes precedence over oauth2_token.

  • oauth2_mechanism"XOAUTH2" (default — Gmail / Microsoft 365 / Yahoo) or "OAUTHBEARER" (Gmail’s standards-track variant)

  • oauth2_vendor – Optional vendor string required by Yahoo’s XOAUTH2 implementation (XOAUTH2 only)

  • envelope_from – Overrides the SMTP envelope “mail from” header

  • subject – The message subject

  • message_headers – Custom message headers

  • attachments – A list of tuples, containing filenames and bytes

  • plain_message – The plain text message body

  • html_message – The HTML message body

  • dkim_private_key – A PEM-encoded RSA private key. When provided (along with dkim_selector and dkim_domain), the message is DKIM-signed before sending.

  • dkim_selector – The DKIM selector to use when signing

  • dkim_domain – The DKIM signing domain (defaults to the domain of message_from when dkim_private_key is set but dkim_domain is not)

  • dkim_additional_headers – Additional header names to include in the DKIM signature. Headers not present in the message are skipped.

mailsuite.dkim

DKIM key management and email signing utilities

exception mailsuite.dkim.DKIMError[source]

Raised when a DKIM error occurs

mailsuite.dkim.generate_dkim_keypair(key_size: int = 2048) tuple[str, str][source]

Generates a DKIM RSA keypair

Parameters:

key_size – The RSA key size in bits (1024 minimum, 2048 recommended)

Returns: A tuple of (private_key_pem, public_key_b64)

mailsuite.dkim.generate_dkim_private_key(key_size: int = 2048) str[source]

Generates a new RSA private key suitable for DKIM signing

Parameters:

key_size – The RSA key size in bits (1024 minimum, 2048 recommended)

Returns: A PEM-encoded private key string

mailsuite.dkim.generate_dkim_txt_record(public_key: str | bytes, selector: str = 'default', domain: str | None = None, flags: str | None = None, note: str | None = None) str[source]

Generates a DKIM TXT record

Parameters:
  • public_key – A base64-encoded public key, or a PEM-encoded private or public key (the base64 is extracted automatically)

  • selector – The DKIM selector

  • domain – An optional domain. When provided, the return value includes the full DNS owner name (selector._domainkey.domain) so it shows exactly where the record must be placed.

  • flags – An optional value for the t= flags tag (e.g. "y" for testing mode)

  • note – An optional value for the n= notes tag

Returns:

When domain is given, the full DNS record (owner name, class, type, and quoted value) as a single line. Otherwise, just the record value (v=DKIM1; ...).

mailsuite.dkim.get_dkim_public_key(private_key: str | bytes) str[source]

Derives the DKIM public key from a private key

Parameters:

private_key – A PEM-encoded RSA private key (PKCS#1 or PKCS#8)

Returns: A base64-encoded SubjectPublicKeyInfo value (suitable for the

p= tag of a DKIM TXT record)

mailsuite.dkim.sign_email(message: str | bytes, selector: str, domain: str, private_key: str | bytes, additional_headers: list[str] | None = None, canonicalize: tuple[bytes, bytes] = (b'relaxed', b'relaxed'), identity: str | None = None) str | bytes[source]

DKIM-signs an email and returns the signed RFC 822 message

The default set of headers signed includes From, To, Cc, Reply-To, Subject, Date, Message-ID, In-Reply-To, References, MIME-Version, Content-Type, Content-Transfer-Encoding, List-Unsubscribe, and List-Unsubscribe-Post — with From, To, Cc, and Subject oversigned (signed twice) to prevent header addition attacks. Headers that are not present in the message are skipped.

Parameters:
  • message – An RFC 822 message

  • selector – The DKIM selector

  • domain – The signing domain

  • private_key – A PEM-encoded RSA private key

  • additional_headers – Additional header names to sign. Headers not present in the message are skipped.

  • canonicalize – A tuple of (header, body) canonicalization algorithms. Defaults to (b"relaxed", b"relaxed").

  • identity – An optional i= value (defaults to @ + domain)

Returns: The signed RFC 822 message. The return type matches the input

type — str in, str out; bytes in, bytes out.

mailsuite.dkim.verify_email(message: str | bytes, timeout: float = 5.0, minkey: int = 1024, dns_func: Callable[[str], bytes] | None = None) dict[source]

Verifies the DKIM signature(s) on an RFC 822 message

Each DKIM-Signature header in the message is verified independently via DNS. The result reports per-signature outcomes plus an overall valid flag (True when at least one signature verifies).

Parameters:
  • message – An RFC 822 message

  • timeout – DNS lookup timeout in seconds

  • minkey – The minimum acceptable RSA key size in bits

  • dns_func – An optional function taking a DNS name and returning the raw TXT record value as bytes. Useful for testing or for using a custom resolver. Defaults to dkimpy’s built-in resolver.

Returns: A dict with the following keys:

  • valid (bool): True if at least one signature verified

  • signatures (list): per-signature results, each a dict with:

    • domain (str): the d= signing domain

    • selector (str): the s= selector

    • valid (bool): whether this signature verified

    • error (str or None): error message when valid is False, otherwise None

mailsuite.arc

Authenticated Received Chain (ARC) sealing and verification (RFC 8617)

ARC lets a sequence of intermediaries (mailing lists, forwarders, gateways) record the email authentication results they observed, so that a later receiver can trust those results even when SPF/DKIM/DMARC break in transit. Each hop adds an ARC set of three header fields keyed by an instance number (i=):

  • ARC-Authentication-Results (AAR) — a snapshot of the Authentication-Results this hop produced.

  • ARC-Message-Signature (AMS) — a DKIM-like signature over the message as this hop saw it.

  • ARC-Seal (AS) — a signature over the ARC header fields, binding the chain together and recording its cumulative validity (cv=).

This module wraps dkimpy’s ARC implementation behind an API shaped like mailsuite.dkim.

exception mailsuite.arc.ARCError[source]

Raised when an ARC error occurs

mailsuite.arc.seal_email(message: str | bytes, selector: str, domain: str, private_key: str | bytes, authserv_id: str, signed_headers: list[str] | None = None, timestamp: int | None = None) str | bytes[source]

Adds an ARC set (seal) to an email and returns the sealed RFC 822 message

The new ARC set is prepended to the message. If the message already carries one or more ARC sets, this adds the next instance and extends the chain.

The message must contain an Authentication-Results header whose authserv-id equals authserv_id — that is the authentication this hop is attesting to, and it is copied into the ARC-Authentication-Results header. Per RFC 8617 the chain is sealed only when such results exist. If none match — or, when extending an existing chain, the matching results record no prior ARC result (arc=) to continue from — no ARC set is produced and ARCError is raised.

Parameters:
  • message – An RFC 822 message

  • selector – The DKIM selector for the sealing domain

  • domain – The sealing (ADMD) domain

  • private_key – A PEM-encoded RSA private key

  • authserv_id – The authentication-service identifier of this hop (the authserv-id used in its Authentication-Results headers, often the receiving host’s name). Only Authentication-Results headers carrying this id are folded into the seal.

  • signed_headers – Header names the ARC-Message-Signature should cover. Defaults to dkimpy’s recommended set — the headers present in the message that it lists as SHOULD-sign (From, To, Cc, Subject, Date, Message-ID, the List-* headers, etc.), with From oversigned. From must be included.

  • timestamp – The t= value (epoch seconds) stamped into the AMS and AS. Defaults to the current time.

Returns: The sealed RFC 822 message. The return type matches the input

type — str in, str out; bytes in, bytes out.

Raises:

ARCError – If the message has no matching Authentication-Results header (nothing to seal), an existing chain cannot be continued, or the inputs are otherwise malformed (e.g. From is not signed).

mailsuite.arc.verify_arc_chain(message: str | bytes, minkey: int = 1024, dns_func: Callable[[str], bytes] | None = None) dict[source]

Verifies the ARC chain on an RFC 822 message

The chain validation value (cv) summarises the whole chain:

  • "pass" — every ARC set verified and the chain is intact.

  • "none" — the message is not ARC sealed.

  • "fail" — the chain is broken (a signature did not verify, a seal reported failure, or an instance reported an invalid status).

Per RFC 8617 the most recent ARC-Message-Signature must validate and every ARC-Seal in the chain must validate for a "pass".

Parameters:
  • message – An RFC 822 message

  • minkey – The minimum acceptable RSA key size in bits

  • dns_func – An optional function taking a DNS name and returning the raw TXT record value as bytes. Useful for testing or for using a custom resolver. Defaults to dkimpy’s built-in resolver.

Returns: A dict with the following keys:

  • valid (bool): True only when cv is "pass"

  • cv (str): the chain validation value ("pass", "fail", or "none")

  • reason (str): a human-readable explanation of the result

  • instances (list): per-ARC-set results in ascending instance order, each a dict with:

    • instance (int): the i= instance number

    • ams_domain (str): the AMS d= signing domain

    • ams_selector (str): the AMS s= selector

    • ams_valid (bool): whether the AMS verified

    • as_domain (str): the AS d= signing domain

    • as_selector (str): the AS s= selector

    • as_valid (bool): whether the AS verified

    • cv (str): the cv= value recorded in this AS

mailsuite.mailbox

Abstract base class for mailbox connections

exception mailsuite.mailbox.base.FolderExistsError[source]

Raised when a folder/label operation targets a name that is already taken — e.g. MailboxConnection.rename_folder() or MailboxConnection.move_folder() onto an existing name.

exception mailsuite.mailbox.base.FolderNotFoundError[source]

Raised when a folder/label referenced by an operation does not exist — e.g. the source of a MailboxConnection.move_folder() / MailboxConnection.merge_folders(), or a destination when its create parameter is left False.

class mailsuite.mailbox.base.MailboxConnection[source]

A provider-agnostic interface for a mailbox

Subclasses implement the methods for a specific protocol (IMAP, Microsoft Graph, Gmail, Maildir, etc.). Methods that don’t apply to a given backend raise NotImplementedError.

create_folder(folder_name: str) None[source]

Create a folder/label in the mailbox

delete_folder(folder_name: str) None[source]

Delete a folder/label from the mailbox

delete_message(message_id: Any) None[source]

Permanently delete a message by identifier

fetch_message(message_id: Any, **kwargs: Any) str[source]

Fetch the raw RFC 822 contents of a message by identifier

fetch_messages(reports_folder: str, **kwargs: Any) list[source]

Return a list of message identifiers in the given folder

folder_exists(folder_name: str) bool[source]

Return True if the named folder/label exists in the mailbox

Parameters:

folder_name – The folder/label name (or path) to check

keepalive() None[source]

Send a no-op to keep the connection alive (if applicable)

merge_folders(sources: str | list[str], destination: str, create: bool = False, keep_source_folders: bool = False) None[source]

Move the contents of one or more folders into another

Every message in each source folder is moved into destination.

Parameters:
  • sources – A source folder path, or a list of them.

  • destination – The folder to move messages into.

  • create – Create destination if it doesn’t already exist. When False (default), a missing destination raises FolderNotFoundError.

  • keep_source_folders – Leave the emptied source folders in place. When False (default), each source folder is deleted after its messages have been moved.

Raises:

FolderNotFoundError – If a source (or, with create=False, the destination) does not exist.

move_folder(source: str, new_path: str | None = None, new_parent: str | None = None, create: bool = False) None[source]

Relocate a folder (and its contents) to a new location

Give exactly one of new_path or new_parent:

  • new_path is the folder’s complete new path, e.g. move_folder("Archive/Forensic", new_path="Reports/Failure").

  • new_parent is the folder to move source under, keeping its own leaf name, e.g. move_folder("Archive/Forensic", new_parent="Reports") yields Reports/Forensic.

Parameters:
  • source – Path of the folder to move. Must exist.

  • new_path – The complete new path for the folder.

  • new_parent – The parent folder to move source under (its leaf name is preserved). Use "" for the mailbox root.

  • create – Create the destination’s parent path if it doesn’t already exist. When False (default), a missing parent raises FolderNotFoundError.

Raises:
  • ValueError – If not exactly one of new_path / new_parent is given.

  • FolderNotFoundError – If source (or, with create=False, the destination parent) does not exist.

  • FolderExistsError – If the target path is already taken.

Note

On Gmail there are no real folders — only labels nested by a / naming convention — so a move renames the label’s path and does not relocate independent descendant labels.

move_message(message_id: Any, folder_name: str) None[source]

Move a message to the named folder

rename_folder(old_name: str, new_name: str) None[source]

Rename a folder/label in the mailbox

Implementations call _ensure_no_folder_conflict() first, so a rename onto an existing name raises FolderExistsError consistently rather than each backend’s native behavior.

Parameters:
  • old_name – The current folder/label name (or path)

  • new_name – The new folder/label name

Raises:

FolderExistsError – If new_name already exists.

send_message(message_from: str, message_to: list[str] | None = None, message_cc: list[str] | None = None, message_bcc: list[str] | None = None, subject: str | None = None, message_headers: dict | None = None, attachments: list[tuple[str, bytes]] | None = None, plain_message: str | None = None, html_message: str | None = None, save_to_sent_items: bool = True) str | None[source]

Send a message through this mailbox’s native send API (when supported)

Backends without a native send (IMAP, Maildir) raise NotImplementedError. Use mailsuite.smtp.send_email() directly when you need to send mail without a mailbox.

Parameters:
  • message_from – The value of the From header

  • message_to – A list of recipient addresses

  • message_cc – A list of Cc addresses

  • message_bcc – A list of Bcc addresses

  • subject – The message subject

  • message_headers – Additional headers

  • attachments – A list of (filename, bytes) tuples

  • plain_message – The plain-text body

  • html_message – The HTML body

  • save_to_sent_items – Whether to save a copy to Sent Items (Microsoft Graph only; Gmail always saves a copy). Default True for backward compatibility.

Returns:

A provider-specific message identifier when available, otherwise None.

watch(check_callback: Callable[[MailboxConnection], None], check_timeout: int, config_reloading: Callable[[], bool] | None = None) None[source]

Watch the mailbox for new messages, invoking check_callback when new mail arrives or on a polling interval

Parameters:
  • check_callback – Called with this MailboxConnection instance whenever the watcher fires.

  • check_timeout – Polling interval (or IDLE timeout) in seconds.

  • config_reloading – Optional zero-argument callable. When it returns a truthy value, the watcher exits cleanly so the caller can reload configuration.

IMAP mailbox backend

class mailsuite.mailbox.imap.IMAPConnection(host: str, user: str, password: str | None = None, port: int = 993, ssl: bool = True, verify: bool = True, timeout: int = 30, max_retries: int = 4, oauth2_token: str | None = None, oauth2_token_provider: Callable[[], str] | None = None, oauth2_mechanism: str = 'XOAUTH2', oauth2_vendor: str | None = None)[source]

A MailboxConnection backed by IMAP

Wraps mailsuite.imap.IMAPClient and adds the MailboxConnection semantics (folder/label management, polling via IDLE, etc.).

IMAP is a mail-access protocol with no send capability — send_message() raises NotImplementedError. Use mailsuite.smtp.send_email() for sending.

create_folder(folder_name: str) None[source]

Create a folder/label in the mailbox

delete_folder(folder_name: str) None[source]

Delete a folder/label from the mailbox

delete_message(message_id: Any) None[source]

Permanently delete a message by identifier

fetch_message(message_id: Any, **kwargs: Any) str[source]

Fetch the raw RFC 822 contents of a message by identifier

fetch_messages(reports_folder: str, **kwargs: Any) list[source]

Return a list of message identifiers in the given folder

folder_exists(folder_name: str) bool[source]

Return True if the named folder/label exists in the mailbox

Parameters:

folder_name – The folder/label name (or path) to check

keepalive() None[source]

Send a no-op to keep the connection alive (if applicable)

move_message(message_id: Any, folder_name: str) None[source]

Move a message to the named folder

rename_folder(old_name: str, new_name: str) None[source]

Rename a folder/label in the mailbox

Implementations call _ensure_no_folder_conflict() first, so a rename onto an existing name raises FolderExistsError consistently rather than each backend’s native behavior.

Parameters:
  • old_name – The current folder/label name (or path)

  • new_name – The new folder/label name

Raises:

FolderExistsError – If new_name already exists.

send_message(*args: Any, **kwargs: Any) str | None[source]

Send a message through this mailbox’s native send API (when supported)

Backends without a native send (IMAP, Maildir) raise NotImplementedError. Use mailsuite.smtp.send_email() directly when you need to send mail without a mailbox.

Parameters:
  • message_from – The value of the From header

  • message_to – A list of recipient addresses

  • message_cc – A list of Cc addresses

  • message_bcc – A list of Bcc addresses

  • subject – The message subject

  • message_headers – Additional headers

  • attachments – A list of (filename, bytes) tuples

  • plain_message – The plain-text body

  • html_message – The HTML body

  • save_to_sent_items – Whether to save a copy to Sent Items (Microsoft Graph only; Gmail always saves a copy). Default True for backward compatibility.

Returns:

A provider-specific message identifier when available, otherwise None.

watch(check_callback: Callable[[MailboxConnection], None], check_timeout: int, config_reloading: Callable[[], bool] | None = None) None[source]

Watch for new messages over an IDLE connection and dispatch each batch to check_callback

Maildir mailbox backend

class mailsuite.mailbox.maildir.MaildirConnection(maildir_path: str, maildir_create: bool = False)[source]

A MailboxConnection backed by an on-disk Maildir

Useful for local processing of messages dropped into a Maildir by an MTA (e.g. postfix delivering DMARC reports). Maildir has no concept of sending — send_message() raises NotImplementedError.

create_folder(folder_name: str) None[source]

Create a folder/label in the mailbox

delete_folder(folder_name: str) None[source]

Delete a folder/label from the mailbox

delete_message(message_id: Any) None[source]

Permanently delete a message by identifier

fetch_message(message_id: Any, **kwargs: Any) str[source]

Fetch the raw RFC 822 contents of a message by identifier

fetch_messages(reports_folder: str, **kwargs: Any) list[source]

Return a list of message identifiers in the given folder

folder_exists(folder_name: str) bool[source]

Return True if the named folder/label exists in the mailbox

Parameters:

folder_name – The folder/label name (or path) to check

keepalive() None[source]

Send a no-op to keep the connection alive (if applicable)

move_message(message_id: Any, folder_name: str) None[source]

Move a message to the named folder

rename_folder(old_name: str, new_name: str) None[source]

Rename a folder/label in the mailbox

Implementations call _ensure_no_folder_conflict() first, so a rename onto an existing name raises FolderExistsError consistently rather than each backend’s native behavior.

Parameters:
  • old_name – The current folder/label name (or path)

  • new_name – The new folder/label name

Raises:

FolderExistsError – If new_name already exists.

send_message(*args: Any, **kwargs: Any) str | None[source]

Send a message through this mailbox’s native send API (when supported)

Backends without a native send (IMAP, Maildir) raise NotImplementedError. Use mailsuite.smtp.send_email() directly when you need to send mail without a mailbox.

Parameters:
  • message_from – The value of the From header

  • message_to – A list of recipient addresses

  • message_cc – A list of Cc addresses

  • message_bcc – A list of Bcc addresses

  • subject – The message subject

  • message_headers – Additional headers

  • attachments – A list of (filename, bytes) tuples

  • plain_message – The plain-text body

  • html_message – The HTML body

  • save_to_sent_items – Whether to save a copy to Sent Items (Microsoft Graph only; Gmail always saves a copy). Default True for backward compatibility.

Returns:

A provider-specific message identifier when available, otherwise None.

watch(check_callback: Callable[[MailboxConnection], None], check_timeout: int, config_reloading: Callable[[], bool] | None = None) None[source]

Watch the mailbox for new messages, invoking check_callback when new mail arrives or on a polling interval

Parameters:
  • check_callback – Called with this MailboxConnection instance whenever the watcher fires.

  • check_timeout – Polling interval (or IDLE timeout) in seconds.

  • config_reloading – Optional zero-argument callable. When it returns a truthy value, the watcher exits cleanly so the caller can reload configuration.

Microsoft Graph mailbox backend

class mailsuite.mailbox.graph.AuthMethod(*values)[source]
class mailsuite.mailbox.graph.MSGraphConnection(auth_method: str, mailbox: str, client_id: str, client_secret: str | None, username: str | None, password: str | None, tenant_id: str, token_file: str, allow_unencrypted_storage: bool, certificate_path: str | None = None, certificate_password: str | bytes | None = None, graph_url: str | None = None, token_cache_name: str = 'mailsuite', client_assertion: str | None = None, client_assertion_provider: Callable[[], str] | None = None)[source]

A MailboxConnection backed by Microsoft Graph

Supports DeviceCode, UsernamePassword, ClientSecret, ClientAssertion, and Certificate auth via azure.identity. Send mail goes through /users/{mailbox}/sendMail with a structured Message body; the request sets saveToSentItems, so a copy is saved to Sent Items by default. Pass save_to_sent_items=False to skip saving a copy.

Required Microsoft Graph API permissions on the app registration (combine as needed):

  • Read-only (fetch_message, fetch_messages): Mail.Read

  • Read + modify (mark read, delete, move, create folder): Mail.ReadWrite

  • Send mail (send_message): Mail.Send

Delegated flows (DeviceCode, UsernamePassword) targeting a shared mailbox (i.e. mailbox != username) use the .Shared variants — Mail.Read.Shared, Mail.ReadWrite.Shared, Mail.Send.Shared. App-only flows (ClientSecret, ClientAssertion, Certificate) do not need the .Shared variants. See the README “Microsoft Graph permissions” section for the full mapping.

Note: delegated flows always request Mail.ReadWrite at authenticate time, so even read-only callers must consent to at least Mail.ReadWrite.

Requires the msgraph extra:

pip install mailsuite[msgraph]
create_folder(folder_name: str) None[source]

Create a folder/label in the mailbox

delete_folder(folder_name: str) None[source]

Delete a folder/label from the mailbox

delete_message(message_id: Any) None[source]

Permanently delete a message by identifier

fetch_message(message_id: Any, **kwargs: Any) str[source]

Fetch the raw RFC 822 contents of a message by identifier

fetch_messages(reports_folder: str, **kwargs: Any) list[str][source]

Return a list of message identifiers in the given folder

folder_exists(folder_name: str) bool[source]

Return True if the folder (by name or parent/child path) resolves to an id, False if no such folder exists. A failed listing call (auth/network) propagates as RuntimeError rather than being reported as a missing folder.

keepalive() None[source]

Send a no-op to keep the connection alive (if applicable)

mark_message_read(message_id: str) None[source]

Mark the message with the given id as read (Graph-only; not part of the MailboxConnection interface)

move_message(message_id: Any, folder_name: str) None[source]

Move a message to the named folder

rename_folder(old_name: str, new_name: str) None[source]

Rename a mail folder in place

Issues PATCH /users/{mailbox}/mailFolders/{id} with a new displayName (requires Mail.ReadWrite). Graph’s update operation only changes the folder’s display name — it does not move the folder to a different parent (relocating a folder is a separate move action). Accordingly, only the leaf segment of new_name is used as the new display name, so passing a parent/child path won’t create a folder whose name literally contains a slash. The folder’s id is unchanged by a rename.

Parameters:
  • old_name – The current folder name or parent/child path

  • new_name – The new display name (leaf segment is used)

Raises:

FolderExistsError – If new_name already resolves to a folder.

send_message(message_from: str, message_to: list[str] | None = None, message_cc: list[str] | None = None, message_bcc: list[str] | None = None, subject: str | None = None, message_headers: dict | None = None, attachments: list[tuple[str, bytes]] | None = None, plain_message: str | None = None, html_message: str | None = None, save_to_sent_items: bool = True) str | None[source]

Send a message through this mailbox’s native send API (when supported)

Backends without a native send (IMAP, Maildir) raise NotImplementedError. Use mailsuite.smtp.send_email() directly when you need to send mail without a mailbox.

Parameters:
  • message_from – The value of the From header

  • message_to – A list of recipient addresses

  • message_cc – A list of Cc addresses

  • message_bcc – A list of Bcc addresses

  • subject – The message subject

  • message_headers – Additional headers

  • attachments – A list of (filename, bytes) tuples

  • plain_message – The plain-text body

  • html_message – The HTML body

  • save_to_sent_items – Whether to save a copy to Sent Items (Microsoft Graph only; Gmail always saves a copy). Default True for backward compatibility.

Returns:

A provider-specific message identifier when available, otherwise None.

watch(check_callback: Callable[[MailboxConnection], None], check_timeout: int, config_reloading: Callable[[], bool] | None = None) None[source]

Poll the mailbox at check_timeout-second intervals

Gmail mailbox backend

class mailsuite.mailbox.gmail.GmailConnection(token_file: str, credentials_file: str, scopes: list[str], include_spam_trash: bool, reports_folder: str, oauth2_port: int, paginate_messages: bool, auth_mode: str = 'installed_app', service_account_user: str | None = None)[source]

A MailboxConnection backed by the Gmail API

Sends mail through users.messages.send with the message built by mailsuite.utils.create_email(). Sending requires a scope that includes the send permission (gmail.send, gmail.modify, or full mail.google.com).

Requires the gmail extra:

pip install mailsuite[gmail]
create_folder(folder_name: str) None[source]

Create a folder/label in the mailbox

delete_folder(folder_name: str) None[source]

Delete a folder/label from the mailbox

delete_message(message_id: Any) None[source]

Permanently delete a message by identifier

fetch_message(message_id: Any, **kwargs: Any) str[source]

Fetch the raw RFC 822 contents of a message by identifier

fetch_messages(reports_folder: str, **kwargs: Any) list[str][source]

Return a list of message identifiers in the given folder

folder_exists(folder_name: str) bool[source]

Return True if the named folder/label exists in the mailbox

Parameters:

folder_name – The folder/label name (or path) to check

keepalive() None[source]

Send a no-op to keep the connection alive (if applicable)

move_message(message_id: Any, folder_name: str) None[source]

Move a message to the named folder

rename_folder(old_name: str, new_name: str) None[source]

Rename a label

Gmail has no folders — only labels — so this backend maps the MailboxConnection “folder” concept onto labels. Renaming patches the label’s display name; the label’s immutable id is preserved, so existing message associations (and any cached id) stay valid.

Only user labels can be renamed. Renaming a system label (INBOX, SENT, SPAM, etc.) is rejected by Gmail and surfaces as a googleapiclient.errors.HttpError. Nested labels are independent: renaming Work does not touch a label named Work/Projects.

Parameters:
  • old_name – The current label name (or id)

  • new_name – The new label display name

Raises:

FolderExistsError – If a label named new_name already exists.

send_message(message_from: str, message_to: list[str] | None = None, message_cc: list[str] | None = None, message_bcc: list[str] | None = None, subject: str | None = None, message_headers: dict | None = None, attachments: list[tuple[str, bytes]] | None = None, plain_message: str | None = None, html_message: str | None = None, save_to_sent_items: bool = True) str | None[source]

Send a message through the Gmail API.

The save_to_sent_items parameter is accepted for API parity with MSGraphConnection.send_message() but is ignored — Gmail always saves a copy to Sent Mail.

watch(check_callback: Callable[[MailboxConnection], None], check_timeout: int, config_reloading: Callable[[], bool] | None = None) None[source]

Poll the mailbox at check_timeout-second intervals

mailsuite.utils

exception mailsuite.utils.EmailParserError[source]

Raised when an email parsing error occurs

mailsuite.utils.convert_outlook_msg(msg_bytes: bytes) str[source]

Uses the msgconvert Perl utility to convert an Outlook .msg file to standard RFC 822 format

Warning

Anomalies are introduced during conversion that make the results unsuitable for forensic analysis.

Parameters:

msg_bytes – the content of the .msg file

Returns: A RFC 822 string

mailsuite.utils.create_email(message_from: str, message_to: list[str] | None = None, message_cc: list[str] | None = None, subject: str | None = None, message_headers: dict | None = None, attachments: list[tuple[str, bytes]] | None = None, plain_message: str | None = None, html_message: str | None = None) str[source]

Creates an RFC 822 email message and returns it as a string

Parameters:
  • message_from – The value of the message from header

  • message_to – A list of addresses to send mail to

  • message_cc – A List of addresses to Carbon Copy (CC)

  • subject – The message subject

  • message_headers – Custom message headers

  • attachments – A list of tuples, containing a filename and bytes

  • plain_message – The plain text message body

  • html_message – The HTML message body

Returns: A RFC 822 email message

mailsuite.utils.decode_base64(data: str) bytes[source]

Decodes a base64 string, with padding being optional

Parameters:

data – A base64 encoded string

Returns: The decoded bytes

mailsuite.utils.from_trusted_domain(message: str | bytes | dict, trusted_domains: list[str] | str, include_sld: bool = True, allow_multiple_authentication_results: bool = False, use_authentication_results_original: bool = False) bool[source]

Checks if an email is from a trusted domain based on the contents of the Authentication-Results header

Warning

Authentication results are not verified by this function, so only use it on emails that have been received by trusted mail servers, and not on third-party emails.

Warning

Set allow_multiple_authentication_results to True if and only if the receiving mail service splits the results of each authentication method in separate Authentication-Results headers and always includes DMARC results.

Warning

Set use_authentication_results_original to True if and only if you use an email security gateway that adds an Authentication-Results-Original header, such as Proofpoint or Cisco IronPort. This does not include API-based email security solutions, such as Abnormal Security.

Parameters:
  • message – An email

  • trusted_domains – A list of trusted domains

  • include_sld – Also return True if the Second-Level Domain (SLD) of an authenticated domain is in trusted_domains

  • allow_multiple_authentication_results – Allow multiple Authentication-Results headers

  • use_authentication_results_original – Use the Authentication-Results-Original header instead of the Authentication-Results header

Returns:

Results of the check

mailsuite.utils.get_filename_safe_string(string: str | None, max_length: int = 146) str[source]

Converts a string to a string that is safe for a filename

Parameters:
  • string – A string to make safe for a filename

  • max_length – Truncate strings longer than this length

Warning

Windows has a 260 character length limit on file paths

Returns: A string safe for a filename

mailsuite.utils.get_reverse_dns(ip_address: str, cache: ExpiringDict | None = None, nameservers: list[str] | None = None, timeout: float = 2.0) str | None[source]

Resolves an IP address to a hostname using a reverse DNS query

Parameters:
  • ip_address – The IP address to resolve

  • cache – Cache storage

  • nameservers – A list of one or more nameservers to use

  • timeout – Sets the DNS query timeout in seconds

Returns: The reverse DNS hostname (if any)

mailsuite.utils.is_outlook_msg(content: bytes) bool[source]

Checks if the given content is an Outlook msg OLE file

Parameters:

content – Content to check

Returns: A flag that indicates if a file is an Outlook MSG file

mailsuite.utils.parse_authentication_results(authentication_results: str | list, from_domain: str | None = None) dict | list[dict][source]

Parses and normalizes an Authentication-Results header value or list of values

Parameters:
  • authentication_results – The value of the header or list of values

  • from_domain – The message From domain

Returns: A parsed header value or list of parsed values

mailsuite.utils.parse_dkim_signature(dkim_signature: str | dict) dict | list[source]

Parses a DKIM-Signature header value or list of values

Parameters:

dkim_signature – A DKIM-Signature header value or list of values

Returns: A parsed DKIM-Signature header value or parsed values

mailsuite.utils.parse_email(data: str | bytes, strip_attachment_payloads: bool = False) dict[source]

A simplified email parser

Parameters:
  • data – RFC 822 message string, or Microsoft .msg bytes

  • strip_attachment_payloads – Remove attachment payloads

Returns: Parsed email data

Note

Attachment dictionaries with binary payloads contain the value binary: True. Use mailsuite.utils.decode_base64 to convert the payload to bytes.

mailsuite.utils.parse_email_address(email_address: tuple | str) dict[source]

Parse an email address into its components

Addresses that email.utils.parseaddr cannot handle are split manually and flagged as noncompliant.

Parameters:

email_address – An address string, or a (display_name, address) tuple as returned by email.utils.parseaddr

Returns:

An OrderedDict with the keys display_name, address, local, domain, sld (the second-level domain), and compliant (False when the address had to be parsed by the fallback splitter)

mailsuite.utils.query_dns(domain: str, record_type: str, cache: ExpiringDict | None = None, nameservers: list[str] | None = None, timeout: float = 2.0)[source]

Queries DNS

Parameters:
  • domain – The domain or subdomain to query about

  • record_type – The record type to query for

  • cache – Cache storage

  • nameservers – A list of one or more nameservers to use

  • timeout – DNS timeout in seconds

Returns:

A list of answers