Metadata-Version: 2.4
Name: exmailer
Version: 1.3.0
Summary: Send emails and calendars via Microsoft Exchange Server with HTML template support
Project-URL: Homepage, https://github.com/aerosadegh/exmailer
Project-URL: Repository, https://github.com/aerosadegh/exmailer
Project-URL: Documentation, https://exmailer.readthedocs.io
Project-URL: Bug Tracker, https://github.com/aerosadegh/exmailer/issues
Author-email: Sadegh Yazdani <sadegh.yazdani@example.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: email,exchange,microsoft,notification,outlook,persian,rtl,smtp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: exchangelib>=5.0.0
Requires-Dist: requests>=2.31.0
Requires-Dist: urllib3>=2.0.0
Provides-Extra: dotenv
Requires-Dist: python-dotenv>=1.2.0; extra == 'dotenv'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
Description-Content-Type: text/markdown

# Ex<span style="color:lightgray">change E</span>mailer 🐍

<div align="center">
  <p><strong>Enterprise-Grade Microsoft Exchange Email &amp; Calendar Client with HTML Template Support</strong></p>
  <p>
    <a href="https://pypi.org/project/exmailer/"><img src="https://img.shields.io/pypi/v/exmailer.svg" alt="PyPI"></a>
    <a href="https://pypi.org/project/exmailer/"><img src="https://img.shields.io/pypi/pyversions/exmailer.svg" alt="Python Versions"></a>
    <a href="https://github.com/aerosadegh/exmailer/actions"><img src="https://github.com/aerosadegh/exmailer/workflows/Publish%20to%20PyPI/badge.svg" alt="Build Status"></a>
    <a href="https://github.com/aerosadegh/exmailer/blob/main/LICENSE"><img src="https://img.shields.io/github/license/aerosadegh/exmailer.svg" alt="License"></a>
  </p>
</div>

**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](https://exmailer.readthedocs.io)** · 🐛 **[Issue Tracker](https://github.com/aerosadegh/exmailer/issues)**

## 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 Variables** — `str.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 Hierarchy** — `AuthenticationError`, `ExchangeEmailConnectionError`, `SendError`, `AttachmentError`, `ConfigurationError`
- **Scoped Debug Logging** — verbose mode logs to a configurable file, without leaking wire-level credentials

## Installation

Requires Python 3.11+

```bash
pip install exmailer

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

Using `uv`:

```bash
uv add exmailer
```

## Quick Start

### Python API

```python
from exmailer import ExchangeEmailer, TemplateType

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

    # English / LTR (default)
    emailer.send_email(
        subject="Weekly Report",
        body="Please find attached.",
        recipients=["colleague@company.com"],
        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:

```python
emailer.send_email(
    subject="System Alert",
    body="Node {node_id} reported status: {status}",
    recipients=["admin@company.com"],
    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

```python
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=["devops@company.com"],
        template="alert",
    )
```

### Calendar Meetings

```python
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=["team@company.com"],
        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

```bash
# Email
python3 -m exmailer \
    --subject "Weekly Report" \
    --body "Report content here" \
    --to recipient@company.com \
    --attachments ./report.pdf

# Read the body from a file
python3 -m exmailer \
    --subject "Maintenance Notice" \
    --body @notice.html \
    --to all-staff@company.com \
    --template persian

# Meeting
python3 -m exmailer --meeting \
    --subject "Deploy Sync" \
    --start "2026-06-25 10:00" --end "2026-06-25 11:00" \
    --to team@company.com \
    --location "Conf Room A"
```

Full CLI reference: [`docs/user-guide/cli-reference.md`](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

```bash
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

```json
{
  "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:

```python
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=["you@company.com"])
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

```bash
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)
