Bote — Secure Python SMTP Client for Plain-Text Email
Bote is a small, security-focused Python library for sending plain-text email via SMTP — a safe wrapper around the standard-library smtplib that enforces STARTTLS/SSL encryption on every remote connection.
Use it to send notifications, alerts, and automated email from your Python applications, whether through a local mail server on localhost or a remote SMTP provider. Bote validates your configuration up front and fails fast with clear, typed errors instead of cryptic SMTP tracebacks.
Features
- Enforced encryption — any connection to an SMTP server other than
localhost/127.0.0.1/::1must use STARTTLS or SSL. This cannot be bypassed. (It does not affect how the SMTP server then relays the message to the recipient.) - Early validation — email addresses and configuration are checked when you create the
Mailer, not when you send, so misconfiguration surfaces immediately. - Clear, typed errors — custom exceptions (
UnencryptedRemoteConnection,NotAnEmail,MissingSubject,MissingMailContent) with helpful messages instead of generic SMTP errors. - Header-injection protection — rejects CR/LF in the subject line to prevent SMTP header injection.
- Type safety — full type hints (PEP 484, ships a
py.typedmarker) for better IDE support and fewer runtime errors. - Configurable timeouts — every SMTP operation honors a timeout so an unresponsive server cannot block forever.
- Automatic text wrapping — messages are wrapped to a configurable width while preserving intentional line breaks.
- Well tested — 100% test coverage across Linux, macOS, and Windows.
"Bote" is German for messenger or courier.
Table of Contents
- Requirements
- Installation
- Quick Start
- Keeping Your Credentials Safe
- Configuration Parameters
- Recipient Options
- Examples
- Error Handling
- API Summary
- Exceptions
- License
- Resources
Requirements
- Python 3.11 or newer (uses modern type-hint syntax such as
dict[str, Any]andstr | None). Tested through Python 3.15. - Dependencies (installed automatically):
compatibility>= 2.3.0, < 3anduserprovided>= 3.0.0, < 4.
Installation
# Using pip
pip install bote
# Using Poetry
poetry add bote
Quick Start
The simplest way to get started is with localhost (no credentials needed):
import bote
# Minimal configuration - only sender and recipient required
mailer = bote.Mailer({
'sender': 'dev@localhost',
'recipient': 'test@example.com'
})
mailer.send_mail('Hello', 'This is a test email')
By default, bote connects to localhost without encryption.
Keeping Your Credentials Safe
You should not store secrets in code that may be shared or saved to source control.
To avoid accidental exposure of secrets it is best practice to use environment variables that can be accessed with os.environ.get(). The python-dotenv could be useful for this too - do not forget to add .env files to .gitignore.
Example using environment variables (optionally with .env):
# .env (do NOT commit this file)
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_USER=app@example.com
SMTP_PASSWORD=super-secret-app-password
SMTP_SENDER=noreply@example.com
SMTP_RECIPIENT=user@example.com
import os
# Optional: load variables from .env during local development
try:
from dotenv import load_dotenv # pip install python-dotenv
load_dotenv()
except Exception:
pass
import bote
mailer = bote.Mailer({
'server': os.environ.get('SMTP_SERVER', 'localhost'),
'server_port': int(os.environ.get('SMTP_PORT', '0')) or None,
'encryption': 'starttls',
'username': os.environ.get('SMTP_USER'),
'passphrase': os.environ.get('SMTP_PASSWORD'),
'sender': os.environ.get('SMTP_SENDER', 'noreply@localhost'),
'recipient': os.environ.get('SMTP_RECIPIENT', 'user@localhost'),
})
mailer.send_mail('Hello', 'Email sent with env-sourced credentials')
Configuration Parameters
Only sender and recipient are required. All other parameters are optional:
| Parameter | Default | Description |
|---|---|---|
server |
localhost |
SMTP server hostname |
server_port |
None |
SMTP server port |
encryption |
off |
Encryption method: 'off', 'starttls', or 'ssl' |
username |
None |
SMTP authentication username |
passphrase |
None |
SMTP authentication password |
wrap_width |
80 |
Line wrap width for email body |
timeout |
60 |
Socket timeout in seconds for SMTP operations |
Important: username and passphrase must be provided together or both omitted.
Recipient Options
The recipient parameter accepts either:
- String: A single email address
- Dictionary: Multiple recipients with keys like
'default'and'admin'
# Single recipient
'recipient': 'user@example.com'
# Multiple recipients
'recipient': {
'default': 'user@example.com',
'admin': 'admin@example.com'
}
When using a dictionary with an 'admin' key, you can use the send_mail_to_admin() shortcut method.
Examples
Here are common production scenarios for sending email via remote SMTP servers.
Localhost with Custom Port
import bote
mailer = bote.Mailer({
'server': 'localhost',
'server_port': 1025,
'recipient': 'test@example.com',
'sender': 'dev@localhost'
})
mailer.send_mail('Test Email', 'Testing with custom port')
STARTTLS (Port 587)
import os
import bote
mail_settings = {
'server': 'smtp.example.com',
'server_port': 587,
'encryption': 'starttls',
'username': os.environ.get('SMTP_USER'),
'passphrase': os.environ.get('SMTP_PASSWORD'),
'recipient': 'user@example.com',
'sender': 'noreply@example.com'
}
mailer = bote.Mailer(mail_settings)
mailer.send_mail('Hello', 'Email sent via STARTTLS')
SSL/TLS (Port 465)
import os
import bote
mail_settings = {
'server': 'smtp.gmail.com',
'server_port': 465,
'encryption': 'ssl',
'username': os.environ.get('GMAIL_USER'),
'passphrase': os.environ.get('GMAIL_APP_PASSWORD'),
'recipient': 'recipient@example.com',
'sender': os.environ.get('GMAIL_USER')
}
mailer = bote.Mailer(mail_settings)
mailer.send_mail('Notification', 'Email sent via SSL/TLS')
Multiple Recipients with Admin
import bote
mail_settings = {
'server': 'smtp.example.com',
'server_port': 587,
'encryption': 'starttls',
'username': 'user@example.com',
'passphrase': 'password',
'recipient': {
'default': 'user@example.com',
'admin': 'admin@example.com'
},
'sender': 'app@example.com'
}
mailer = bote.Mailer(mail_settings)
# Send to default recipient
mailer.send_mail('Update', 'New features available')
# Send to admin
mailer.send_mail_to_admin('Alert', 'System requires attention')
# Override recipient for one message
mailer.send_mail('Custom', 'One-off message', overwrite_recipient='other@example.com')
Error Handling
Bote validates configuration when you create the Mailer, so most mistakes are caught before you ever try to send. Sending then raises clear, catchable exceptions:
import bote
try:
mailer = bote.Mailer({
'server': 'smtp.example.com',
'server_port': 587,
'encryption': 'starttls',
'username': 'user@example.com',
'passphrase': 'app-password',
'sender': 'noreply@example.com',
'recipient': 'user@example.com',
})
mailer.send_mail('Subject', 'Body text')
except bote.UnencryptedRemoteConnection:
print("Refused: remote connections must use STARTTLS or SSL.")
except bote.NotAnEmail:
print("One of the addresses is not a valid email.")
except (bote.MissingSubject, bote.MissingMailContent):
print("A subject and a body are both required.")
All custom exceptions inherit from bote.BoteException, so you can catch them all with a single except bote.BoteException:.
API Summary
Constructor
bote.Mailer(mail_settings: dict)
Creates a mailer instance with validated settings.
Required settings:
sender(str): Sender email addressrecipient(str | dict): Recipient email address or dictionary with 'default'/'admin' keys
Optional settings:
server(str): SMTP server hostname (default:'localhost')server_port(int): SMTP server port (default:None)encryption(str): Encryption method -'off','starttls', or'ssl'(default:'off')username(str): SMTP authentication username (default:None)passphrase(str): SMTP authentication password (default:None)wrap_width(int): Line wrap width for email body (default:80)timeout(float): Socket timeout in seconds for all SMTP operations (default:60)
Note: username and passphrase must be provided together or both omitted.
Methods
send_mail(message_subject: str, message_text: str, overwrite_recipient: str | None = None)
Send an email with the given subject and body text. Optionally override the default recipient.
send_mail_to_admin(message_subject: str, message_text: str)
Send an email to the admin address (requires recipient to be a dictionary with an 'admin' key).
Exceptions
Bote raises custom exceptions for common error cases. They are exported at the top level (bote.NotAnEmail) and are also available from the bote.err module:
bote.UnencryptedRemoteConnection: Raised when attempting to connect to a remote SMTP server without encryptionbote.NotAnEmail: Raised when an invalid email address is providedbote.MissingSubject: Raised when attempting to send an email without a subject linebote.MissingMailContent: Raised when attempting to send an email without body content
All exceptions inherit from bote.BoteException, which inherits from the standard Exception class, so except bote.BoteException: catches every bote-specific error.
License
Bote is released under the Apache License 2.0.
Resources
- Changelog: CHANGELOG.md
- Security policy / reporting a vulnerability: SECURITY.md
- Contributing: contributing.md
- Issue tracker: https://github.com/RuedigerVoigt/bote/issues
- PyPI: https://pypi.org/project/bote/
Metadata
Release files for bote 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bote-2.0.0.tar.gz | 17.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bote-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.2 kB
Release files / bote-2.0.0.tar.gz
| Download URL | bote-2.0.0.tar.gz |
|---|---|
| Size | 17.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1124b12bd1129999fe2eefa7ec9543d909425e98c42bf242a84502eb4f04a61c
|
|
BLAKE2b-256 checksum How to use checksums |
b346a5de97920842c06c7474e02ff961c29848951adf9153dfd318343758907f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / bote-2.0.0-py3-none-any.whl
| Download URL | bote-2.0.0-py3-none-any.whl |
|---|---|
| Size | 15.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b796ad188287eee2cc02e71b904eaf9cd513f9b998993d31b25cd8aec0876141
|
|
BLAKE2b-256 checksum How to use checksums |
5c772e0ad0655814b1fe3c690fe12ebd85cbe9c1afba622c4d9378ad291084a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log