Modern sync and async SMTP with optional TLS/SSL, OAuth2 XOAUTH2, Jinja2 templates, and attachments.
Project description
MailToolsBox
MailToolsBox is a modern, pragmatic email toolkit for Python. It gives you clean, production‑grade SMTP sending and a capable IMAP client in one package. The design favors explicit security controls, sane defaults, and simple APIs that scale from quick scripts to services.
What you get
- SMTP sender with sync and async APIs
- IMAP client with search, fetch, flags, move, delete, export
- Security modes:
auto,starttls,ssl,none - Optional OAuth2 XOAUTH2 for both SMTP and IMAP
- Gmail and Exchange Online presets
- Jinja2 templates with auto plain text fallback
- MIME smart attachment handling
- Bulk sending helpers
- Optional email validation (opt-in extra)
- Environment variable configuration
- Backward compatibility shim
SendAgent
Install
pip install MailToolsBox
# Optional: include address validation support
pip install "MailToolsBox[validation]"
Quick start
Send a basic email
from MailToolsBox import EmailSender
sender = EmailSender(
user_email="you@example.com",
server_smtp_address="smtp.example.com",
user_email_password="password",
port=587, # typical for STARTTLS
security_mode="starttls" # or "auto"
# validation is off by default to keep dependencies light;
# enable with validate_emails=True after installing the [validation] extra
)
sender.send(
recipients=["to@example.com"],
subject="Hello",
message_body="Plain text body"
)
Read emails
from MailToolsBox import ImapClient
with ImapClient(
email_account="you@example.com",
password="password",
server_address="imap.example.com",
port=993,
security_mode="ssl"
) as imap:
imap.select("INBOX")
uids = imap.search("UNSEEN")
messages = imap.fetch_many(uids[:10])
for m in messages:
print(m.subject, m.from_[0].email if m.from_ else None)
Tip: If you installed the package as a single module, import paths may be
from MailToolsBox import ImapClient. Keep them consistent with your package layout.
SMTP in depth
Security modes
auto:- If port is 465 use implicit SSL.
- Otherwise attempt STARTTLS if the server advertises it. If not available, stay plain.
starttls: force STARTTLS upgrade.ssl: implicit SSL on connect, typical for port 465.none: no TLS. Use only inside trusted networks.
You can also pass use_tls=True to force STARTTLS regardless of the configured security_mode (or use_tls=False to force plaintext for trusted relays).
Gmail and Exchange recipes
# Gmail with app password
sender = EmailSender.for_gmail_app_password("you@gmail.com", "abcd abcd abcd abcd")
sender.send(["to@example.com"], "Hi", "Body")
# Exchange Online with SMTP AUTH
exchange = EmailSender.for_exchange_smtp_auth("you@company.com", "password")
exchange.send(["person@company.com"], "Status", "Body")
# Exchange on-prem with self-signed certs (only on trusted networks)
exchange_on_prem = EmailSender(
user_email="you@corp.local",
server_smtp_address="mail.corp.local",
user_email_password="password",
port=587,
security_mode="starttls",
allow_invalid_certs=True, # accept self-signed certs on trusted networks
)
exchange_on_prem.send(["admin@corp.local"], "Status", "Body")
OAuth2 XOAUTH2
oauth_sender = EmailSender(
user_email="you@gmail.com",
server_smtp_address="smtp.gmail.com",
port=587,
security_mode="starttls",
oauth2_access_token="ya29.a0Af..." # obtain via your OAuth flow
)
oauth_sender.send(["to@example.com"], "OAuth2", "Sent with XOAUTH2")
HTML with plain fallback and attachments
html = "<h1>Report</h1><p>See attachment.</p>"
sender.send(
recipients=["to@example.com"],
subject="Monthly report",
message_body=html,
html=True,
attachments=["/path/report.pdf", "/path/chart.png"]
)
Async sending
import asyncio
async def main():
await sender.send_async(
recipients=["to@example.com"],
subject="Async",
message_body="Non blocking send"
)
asyncio.run(main())
Bulk helpers
sender.send_bulk(
recipients=["a@example.com", "b@example.com"],
subject="Announcement",
message_body="Sent individually to protect privacy"
)
Environment variables
export EMAIL=you@example.com
export EMAIL_PASSWORD=apppass
export SMTP_SERVER=smtp.gmail.com
export SMTP_PORT=465
export EMAIL_SECURITY=ssl
export EMAIL_REPLY_TO=noreply@example.com
sender = EmailSender.from_env()
IMAP in depth
The ImapClient provides safe defaults with flexible control when you need it.
Connect and select
imap = ImapClient(
email_account="you@example.com",
password="password",
server_address="imap.example.com",
port=993,
security_mode="ssl",
allow_invalid_certs=False, # set True to accept self-signed certs
)
imap.login()
imap.select("INBOX")
Or use context manager:
with ImapClient.from_env() as imap:
imap.select("INBOX")
print(imap.list_mailboxes())
Environment variables:
export IMAP_EMAIL=you@example.com
export IMAP_PASSWORD=apppass
export IMAP_SERVER=imap.gmail.com
export IMAP_PORT=993
export IMAP_SECURITY=ssl
# Optional OAuth token
export IMAP_OAUTH2_TOKEN=ya29.a0Af...
# Accept self-signed certs for dev/on-prem only
export IMAP_ALLOW_INVALID_CERTS=1
Search and fetch
uids = imap.search("UNSEEN", "SINCE", "01-Jan-2025")
item = imap.fetch(uids[0])
print(item.subject, item.date, item.flags)
print(item.text or item.html)
Attachments and export
paths = imap.save_attachments(item, "./attachments")
eml_path = imap.save_eml(item, "./message.eml")
Flags, move, delete
imap.mark_seen(item.uid)
imap.add_flags(item.uid, "\Flagged")
imap.move([item.uid], "Archive")
imap.delete(item.uid)
imap.expunge()
Legacy style exports
# Dump mailbox to one text file
imap.download_mail_text(path="./dumps", mailbox="INBOX")
# Export selected emails as JSON
imap.download_mail_json(lookup="UNSEEN", save=True, path="./dumps", file_name="mail.json")
# Save each message to .eml
imap.download_mail_eml(directory="./eml", lookup="ALL", mailbox="INBOX")
OAuth2 XOAUTH2
imap = ImapClient(
email_account="you@gmail.com",
password=None,
server_address="imap.gmail.com",
port=993,
security_mode="ssl",
oauth2_access_token="ya29.a0Af..."
)
with imap:
imap.select("INBOX")
uids = imap.search("ALL")
Validation and templates
- Addresses are normalized with
email-validatoronly when you opt in viavalidate_emails=Trueand install the optional extra:pip install "MailToolsBox[validation]". - Templates use Jinja2 with autoescape for HTML and XML.
- HTML sending includes a plain text alternative for better deliverability.
Template example templates/welcome.html:
<h1>Welcome, {{ user }}</h1>
<p>Activate your account: <a href="{{ link }}">activate</a></p>
Send with template:
sender = EmailSender(
user_email="you@example.com",
server_smtp_address="smtp.example.com",
user_email_password="pw",
template_dir="./templates"
)
sender.send_template(
recipient="to@example.com",
subject="Welcome",
template_name="welcome.html",
context={"user": "Alex", "link": "https://example.com/activate"}
)
Backward compatibility
SendAgent stays available for older codebases. It is thin and delegates to EmailSender. Prefer EmailSender in new code.
from MailToolsBox import SendAgent
legacy = SendAgent("you@example.com", "smtp.example.com", "pw", port=587)
legacy.send_mail(["to@example.com"], "Subject", "Body", tls=True)
ImapAgent remains as a thin adapter over ImapClient for projects that still import the legacy name.
Security notes
- Prefer
sslon 465 orstarttlson 587. - To accept self-signed certificates for SMTP/IMAP on trusted networks, pass
allow_invalid_certs=True. - To intentionally skip TLS (inside trusted networks only), use
security_mode="none". - For on-prem Exchange or legacy servers with self-signed certificates, pass
allow_invalid_certs=True(SMTP/IMAP) only on trusted networks. - Use app passwords when your provider offers them.
- Prefer OAuth2 tokens for long term services.
- Use
noneonly on trusted networks.
Testing
pip install -e ".[dev]" # or: pip install -r requirements-dev.txt
pytest
Tests are network-free and rely on local fakes, so they run quickly.
Troubleshooting
- Authentication errors on Gmail usually mean you need an app password or OAuth2.
- If a STARTTLS upgrade fails in
auto, setsecurity_mode="ssl"on 465 orsecurity_mode="starttls"on 587. - For corporate relays that do not support TLS, set
security_mode="none"and ensure the network is trusted. - Enable logging in your application to capture SMTP or IMAP server responses.
import logging
logging.basicConfig(level=logging.INFO)
Contributing
PRs are welcome. Keep changes focused and covered with tests. Add docs for new behavior. Use ruff and black for formatting.
License
MIT. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mailtoolsbox-2.0.0.tar.gz.
File metadata
- Download URL: mailtoolsbox-2.0.0.tar.gz
- Upload date:
- Size: 20.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b1f74b0d0b326c7f497201bd6450e3a65fdb285e9b027005ba0a7a8bdcda0c
|
|
| MD5 |
970c361e67d0dcb21e592cb2fc64a371
|
|
| BLAKE2b-256 |
24ccf246cf6dbdcd190b1fecfa05e42493c957fd9714660327813f8f02cc3e3d
|
File details
Details for the file mailtoolsbox-2.0.0-py3-none-any.whl.
File metadata
- Download URL: mailtoolsbox-2.0.0-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3919cee1bcc1a9310fac70d923ada486f8dcee042b08c68f82a71664252b7c79
|
|
| MD5 |
32eb5f184211ba0a82c4dc835ba9281b
|
|
| BLAKE2b-256 |
0c99478f4d635b1279c792fa04f18af1479dacb845ce7c5f116a6ac48bdf9db1
|