Skip to content

aerosadegh/exmailer

Repository files navigation

Exchange Emailer 🐍

Enterprise-Grade Microsoft Exchange Email & Calendar Client with HTML Template Support

PyPI Python Versions Build Status License

ExMailer is a Python library for interacting with Microsoft Exchange Servers (EWS). It handles NTLM authentication, provides a flexible HTML templating engine with first-class RTL/Persian support, and covers both email and calendar operations.

📖 Full Documentation · 🐛 Issue Tracker

Features

  • Microsoft Exchange Integration — NTLM (and BASIC) authentication over EWS
  • Email & Calendar — send emails and create, update, or cancel meeting invitations
  • HTML Templating — built-in Persian (RTL) and English (LTR) templates, plus a registry for custom layouts
  • Template Variablesstr.format()-style substitution in both body and template
  • Attachments — multiple files with automatic MIME-type detection for common formats
  • Flexible Configuration — programmatic dict, JSON/YAML config files, or environment variables (layered)
  • Timezone-aware Meetings — configurable IANA timezone with startup validation
  • CLI Interface — send emails and schedule meetings from the shell
  • Secure by Default — system SSL certificate verification (opt-in bypass with loud warning)
  • Typed Exception HierarchyAuthenticationError, ExchangeEmailConnectionError, SendError, AttachmentError, ConfigurationError
  • Scoped Debug Logging — verbose mode logs to a configurable file, without leaking wire-level credentials

Installation

Requires Python 3.11+

pip install exmailer

# Optional extras
pip install exmailer[yaml]      # YAML config file support
pip install exmailer[dotenv]    # load .env files

Using uv:

uv add exmailer

Quick Start

Python API

from exmailer import ExchangeEmailer, TemplateType

with ExchangeEmailer() as emailer:
    # Persian / RTL
    emailer.send_email(
        subject="گزارش هفتگی",
        body="لطفاً گزارش پیوست شده را بررسی نمایید.",
        recipients=["[email protected]"],
        template=TemplateType.PERSIAN,
        attachments=["./report.pdf"],
    )

    # English / LTR (default)
    emailer.send_email(
        subject="Weekly Report",
        body="Please find attached.",
        recipients=["[email protected]"],
        template=TemplateType.DEFAULT,
        attachments=["./report.pdf"],
    )

The default template is TemplateType.DEFAULT (English LTR). Pass template=None to send without any wrapper.

Template Variables

Both the body and the template receive the same variables:

emailer.send_email(
    subject="System Alert",
    body="Node {node_id} reported status: {status}",
    recipients=["[email protected]"],
    template_vars={"node_id": "SRV-01", "status": "CRITICAL"},
)

Escaping literal braces: if your body contains inline CSS or JavaScript (e.g. body { color: red; }) and you pass template_vars, escape braces as {{ and }}. Unescaped braces cause body substitution to be skipped (with a logged warning); the email is still sent, but placeholders remain literal.

Custom Templates

from exmailer import ExchangeEmailer, register_custom_template

register_custom_template("alert", """
<div style="border: 1px solid #ccc; padding: 20px;">
    <h1 style="color: navy;">Company Alert</h1>
    {body}
    <hr>
    <small>Confidential</small>
</div>
""")

with ExchangeEmailer() as emailer:
    emailer.send_email(
        subject="Server Down",
        body="<p>The main database is unreachable.</p>",
        recipients=["[email protected]"],
        template="alert",
    )

Calendar Meetings

import datetime
from zoneinfo import ZoneInfo
from exmailer import ExchangeEmailer, TemplateType

tz = ZoneInfo("Asia/Tehran")

with ExchangeEmailer() as emailer:
    exchange_id = emailer.send_meeting_invite(
        subject="Sprint Planning",
        start=datetime.datetime(2026, 6, 25, 10, 0, tzinfo=tz),
        end=datetime.datetime(2026, 6, 25, 11, 0, tzinfo=tz),
        body="<p>Agenda: backlog grooming and sprint goals.</p>",
        required_attendees=["[email protected]"],
        location="Conference Room A",
        template=TemplateType.PERSIAN,
    )

    # Reschedule (by default only attendees whose data changed are notified)
    emailer.update_meeting_invite(
        exchange_id=exchange_id,
        subject="Sprint Planning (rescheduled)",
        start=datetime.datetime(2026, 6, 25, 14, 0, tzinfo=tz),
        end=datetime.datetime(2026, 6, 25, 15, 0, tzinfo=tz),
    )

    # Cancel
    emailer.cancel_meeting_invite(exchange_id=exchange_id)

Naive datetimes are accepted — they are stamped with the timezone from your configuration, or with the system's local timezone if none is configured.

CLI

# Email
python3 -m exmailer \
    --subject "Weekly Report" \
    --body "Report content here" \
    --to [email protected] \
    --attachments ./report.pdf

# Read the body from a file
python3 -m exmailer \
    --subject "Maintenance Notice" \
    --body @notice.html \
    --to [email protected] \
    --template persian

# Meeting
python3 -m exmailer --meeting \
    --subject "Deploy Sync" \
    --start "2026-06-25 10:00" --end "2026-06-25 11:00" \
    --to [email protected] \
    --location "Conf Room A"

Full CLI reference: docs/user-guide/cli-reference.md.

Configuration

Settings are resolved with the following priority (highest first):

  1. Programmatic dictionary passed to ExchangeEmailer(config={...})
  2. Explicit config file path passed to ExchangeEmailer(config_path="...")
  3. Auto-discovered exmailer.json / exmailer.yaml in ./ or ~/.config/exmailer/
  4. Environment variables (fill any keys still missing after the layers above)

Environment variables

EXCHANGE_DOMAIN="CORP"
EXCHANGE_USER="jdoe"
EXCHANGE_PASS="secret_password"
EXCHANGE_SERVER="mail.corp.com"
EXCHANGE_EMAIL_DOMAIN="corp.com"
EXCHANGE_AUTH_TYPE="NTLM"        # or BASIC (default: NTLM)
EXCHANGE_SAVE_COPY="true"        # default: true
EXCHANGE_VERIFY_SSL="true"       # default: true — set to false for self-signed servers (warns loudly)

JSON / YAML file

{
  "domain": "CORP",
  "username": "jdoe",
  "password": "secret_password",
  "server": "mail.corp.com",
  "email_domain": "corp.com",
  "auth_type": "NTLM",
  "save_copy": true,
  "verify_ssl": true,
  "exchange_build": [15, 1, 2248, 0],
  "timezone": "Asia/Tehran"
}
Key Required Default Description
domain Yes Active Directory domain (e.g. CORP)
username Yes Account name without domain (e.g. jdoe)
password Yes Account password
server Yes Exchange server hostname
email_domain Yes Email address domain (e.g. corp.com)
auth_type No NTLM NTLM or BASIC
save_copy No true Save a copy of sent items in Sent Items
verify_ssl No true Disable only for trusted internal networks with self-signed certs; a warning is logged
exchange_build No [15, 1, 2248, 0] Exchange server build. Accepts either [15, 1, 2248, 0] (Exchange 2016) or "15.2.986.0" (Exchange 2019). Invalid values fall back to the default with a warning.
timezone No system local timezone IANA timezone (e.g. "Asia/Tehran", "UTC") attached to naive datetimes in meeting methods. Invalid names raise ConfigurationError at startup.

Exception Handling

All ExMailer errors inherit from ExchangeEmailerError, so you can catch them collectively or individually:

from exmailer import (
    ExchangeEmailer,
    ExchangeEmailerError,
    AuthenticationError,
    ExchangeEmailConnectionError,
    SendError,
    AttachmentError,
    ConfigurationError,
)

try:
    with ExchangeEmailer(config=config) as emailer:
        emailer.send_email(subject="Hi", body="Test", recipients=["[email protected]"])
except AuthenticationError as e:
    print(f"Bad credentials: {e}")
except ExchangeEmailConnectionError as e:
    print(f"Cannot reach Exchange: {e}")
except SendError as e:
    print(f"Delivery failed: {e}")
except ExchangeEmailerError as e:
    print(f"Other ExMailer error: {e}")

Requirements

  • Python 3.11+
  • Access to a Microsoft Exchange Server (EWS endpoint)
  • Valid domain credentials

Development

git clone https://github.com/aerosadegh/exmailer.git
cd exmailer
uv sync --all-extras
uv run pytest

Runs the full suite (unit tests use mocked EWS — no real Exchange required).

Author

Sadegh Yazdani

License

GNU General Public License v3 (GPLv3)

About

A Python package for sending emails via Microsoft Exchange Server

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Contributors