Skip to main content

btx_lib_mail

CI CodeQL License: MIT Open in Codespaces PyPI PyPI - Downloads Code Style: Ruff codecov Maintainability security: bandit

Send email from Python and from the shell.

Most mail libraries are sold on a feature list, and nobody loses sleep over a feature list. What keeps you up is the one message that matters failing at the worst possible time: the 2 GB export that runs your server out of memory, the customer whose name comes through as garbled bytes, the primary relay that drops mid-send. btx_lib_mail is built for that message, not for the demo.

What you get:

  • Send a 100 GB attachment on a box with almost no free memory. The message is streamed to disk and pushed to the server one chunk at a time, so peak memory stays flat whether the file is 100 kB or 100 GB. It never holds the whole payload in RAM, because the RAM you do not have is the RAM that kills the job. (Attachments are capped at 25 MiB by default; raise attachment_max_size_bytes for the big ones. The trade is scratch disk of about 1.37x the attachment, once, however many recipients.)
  • BDAT when the server supports it, DATA when it does not. If the far end advertises CHUNKING (RFC 3030) the message goes out in length-prefixed chunks; otherwise it falls back to a correct, dot-stuffed DATA phase. You do not pick, the library negotiates.
  • Subjects and bodies that encode correctly. Umlauts, emoji, CJK, a subject that is only a full stop: all handled by the standard library's modern email machinery, not by a regex someone wrote at 2am. No more Grüße arriving as Grüße in a customer's name.
  • Internationalised addresses (RFC 6531 SMTPUTF8). empfänger@müller.de works as sender or recipient, NFC-normalised, and is sent as UTF-8 to relays that offer SMTPUTF8; ASCII mail is unchanged.
  • No more hand-rolling smtplib. No MIME assembly, no dot-stuffing, no failover loop. One function call, or one command.
  • A command-line mailer. btx-lib-mail send ... turns any shell into a mail client, with the same streaming and the same checks.
  • Attachments checked before they leave. Path traversal, symlinks, /.ssh/, system directories, dangerous extensions (Windows and Unix executables alike) and a size cap are refused by default, so you do not email your private key by accident. Each file is opened once, right after its checks, and the bytes sent are the bytes of the file that was checked.
  • One error type to catch. Every refusal by the library and every delivery failure is a BtxMailError, and the CLI has --json output and documented exit codes for scripts and agents.

The part that costs us and earns your trust is the unglamorous part: every wire path is proven end to end against a real SMTP server, including BDAT, STARTTLS with authentication, and a memory bound that does not grow with the attachment. That confidence is the actual product. Go and try the daft thing: point it at a spare mailbox and attach something absurd.

Quickstart

Install with uv, with a cooldown that skips releases younger than 7 days (why):

# install uv first, if you do not have it (macOS / Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

uv tool install --exclude-newer "7 days" btx_lib_mail    # the CLI on your PATH

To use the library in a project, add [tool.uv] exclude-newer = "7 days" to its pyproject.toml, then uv add btx_lib_mail. Plain pip install btx_lib_mail works too, without the cooldown.

Send an email from Python:

from btx_lib_mail import send

send(
    mail_from="alerts@example.com",
    mail_recipients=["oncall@example.com"],
    mail_subject="build failed",
    mail_body="See CI logs for details.",
    smtphosts=["smtp.example.com:587"],
    credentials=("mailer", "DUMMY-PLANTED-password"),
)

Send to an internationalised address (the relay must offer SMTPUTF8):

send(mail_from="alerts@example.com", mail_recipients=["empfänger@müller.de"], mail_subject="Grüße", mail_body="Hallo.", smtphosts=["smtp.example.com:587"])

Attach a huge file without running out of memory (streamed, never buffered whole):

from pathlib import Path
from btx_lib_mail import send

send(
    mail_from="backups@example.com",
    mail_recipients=["archive@example.com"],
    mail_subject="nightly dump",
    mail_body="Attached.",
    smtphosts=["smtp.example.com:587"],
    attachment_file_paths=[Path("/data/nightly-dump.tar")],
    attachment_max_size_bytes=50 * 1024**3,  # the default cap is 25 MiB; raise it for big files
)

Send from the shell:

btx-lib-mail send \
  --host smtp.example.com:587 \
  --sender alerts@example.com \
  --recipient oncall@example.com \
  --subject "Ping" \
  --body "Smoke test"

Both commands (btx_lib_mail and btx-lib-mail) and python -m btx_lib_mail run the same CLI.

smtp_password is a SecretStr (masked in repr() and logs); see Configuration - Credentials for non-ASCII passwords, numeric passwords from environment layers, and what to keep out of a custom validator's error message.

Use send(config=...) instead of the global conf when an application holds its own settings object (a worker with per-tenant credentials, a test that must not touch global state):

from btx_lib_mail import ConfMail, send

tenant_config = ConfMail(
    smtphosts=["smtp.example.com:587"],
    smtp_username="mailer",
    smtp_password="DUMMY-PLANTED-password",
)
send(
    mail_from="alerts@example.com",
    mail_recipients=["oncall@example.com"],
    mail_subject="build failed",
    mail_body="See CI logs for details.",
    config=tenant_config,  # every value not also passed explicitly comes from here, not conf
)

ConfMail refuses a name it does not have, and its field names are not the send() keyword names: ConfMail(use_starttls=False) raises ConfigurationError (a pydantic ValidationError); the field is smtp_use_starttls.

Use it from an AI agent (zero install)

btx_lib_mail is built to be driven by LLMs and agents, not only by people. An agent can send mail with nothing installed, straight from PyPI via uvx (the cooldown skips releases younger than 7 days):

uvx --exclude-newer "7 days" btx-lib-mail send \
  --host smtp.example.com:587 \
  --sender alerts@example.com --recipient oncall@example.com \
  --subject "Ping" --body "Smoke test"

The library also ships a Claude Code skill (python-send-mail) that teaches an agent when and how to use it: install, uvx, the library API, testing through the Transport seam, the CLI (--json, --env-file, --password-file), streaming and BDAT, and attachment security. Install it into any project:

/plugin marketplace add bitranox/btx_lib_mail
/plugin install btx_lib_mail

It is also available in the central bitranox-skills marketplace as coding-python-send-mail.

Architecture

The package is layered, and make test enforces it with one import-linter layers contract ([tool.importlinter] in pyproject.toml): a module imports only from the layers below its own, and the modules sharing a layer do not import each other.

Layer (top to bottom) Responsibility
cli The rich-click commands, settings sources, output modes, exit codes
lib_mail send(), the public names, host order and failover, the failure log
_compose The message: headers per recipient, the body and attachments encoded once
_config, _transport ConfMail; the Transport seam, BDAT/DATA streaming and the deadline
_attachments, _validation Attachment security and the open-once file; address, host, number checks
secret_safety, errors Credential-safe validation errors; the BtxMailError family
_common, _descriptor_path Logger and printable text; the path the system holds for an open file
behaviors Scaffold helpers

Module reference describes each module.

Documentation

Project docs

Metadata

Release files for btx-lib-mail 4.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for btx-lib-mail 4.2.0
File Size Uploaded
btx_lib_mail-4.2.0.tar.gz 285.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for btx-lib-mail 4.2.0
File Interpreter ABI Platform
btx_lib_mail-4.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 395.5 kB

Release files / btx_lib_mail-4.2.0.tar.gz

Download URL btx_lib_mail-4.2.0.tar.gz
Size 285.9 kB
Tags Source
SHA-256 checksum
How to use checksums
32d4f4f0356e847c14ba1b9d19124acff9b69ac0dce6aa5da13dad1ca476ebdb
BLAKE2b-256 checksum
How to use checksums
3d5825f98096d08eb733b856b3551bb4fa52ad36eddc0d40e47775a0db2adcea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / btx_lib_mail-4.2.0-py3-none-any.whl

Download URL btx_lib_mail-4.2.0-py3-none-any.whl
Size 109.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d7d78d4f83e249bb293ac527bb92b106e40b0e6d5e9a3f838e90cefe8e7657c
BLAKE2b-256 checksum
How to use checksums
18b257285a6b40018d165ac7f7ab87c14054d3c6ef768dab7e08dff2f3921e87
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

4.5.0

2 release files

4.4.0

2 release files

4.3.1

2 release files

4.3.0

2 release files

This release

4.2.0 This release

2 release files

4.1.0

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.0.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.2

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page