Skip to main content

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-validator only when you opt in via validate_emails=True and 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 ssl on 465 or starttls on 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 none only 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, set security_mode="ssl" on 465 or security_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mailtoolsbox-2.0.0.tar.gz (20.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mailtoolsbox-2.0.0-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

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

Hashes for mailtoolsbox-2.0.0.tar.gz
Algorithm Hash digest
SHA256 b5b1f74b0d0b326c7f497201bd6450e3a65fdb285e9b027005ba0a7a8bdcda0c
MD5 970c361e67d0dcb21e592cb2fc64a371
BLAKE2b-256 24ccf246cf6dbdcd190b1fecfa05e42493c957fd9714660327813f8f02cc3e3d

See more details on using hashes here.

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

Hashes for mailtoolsbox-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3919cee1bcc1a9310fac70d923ada486f8dcee042b08c68f82a71664252b7c79
MD5 32eb5f184211ba0a82c4dc835ba9281b
BLAKE2b-256 0c99478f4d635b1279c792fa04f18af1479dacb845ce7c5f116a6ac48bdf9db1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page