Skip to content

Myth7x/msgraph-smtp-adapter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MS Graph SMTP Adapter

SMTP server that relays received emails to Microsoft Graph API for sending. Supports multiple user-to-mailbox mappings, STARTTLS encryption, and basic SMTP authentication.

Overview

This adapter serves as an SMTP server that accepts authenticated email submissions and relays them via Microsoft Graph's sendMail API. Designed for scenarios where you need to route SMTP traffic through Azure/Office 365 mailboxes.

Key features:

  • SMTP server with STARTTLS support
  • SMTP authentication (LOGIN and PLAIN mechanisms)
  • Multiple user-to-mailbox mapping
  • Microsoft Graph Client Credentials OAuth flow
  • Exponential backoff retry logic (3 attempts)
  • Mock client for testing without Graph credentials
  • Email parsing and relay with CC/BCC support

Prerequisites

  • Python 3.10+
  • pip
  • Virtual environment (venv)
  • TLS certificate and key (self-signed or CA-signed)
  • For production: MS Graph application registration with Mail.Send permission

Setup

1. Create Virtual Environment

cd msgraph-smtp-adapter
python3 -m venv .venv

2. Activate Virtual Environment

Linux/macOS:

source .venv/bin/activate

Windows:

.venv\Scripts\activate

3. Install Dependencies

pip install -r requirements.txt

4. Generate TLS Certificate (Testing)

For self-signed certificate:

mkdir -p certs
openssl req -x509 -newkey rsa:2048 -keyout certs/smtp.key -out certs/smtp.crt \
  -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Org/CN=localhost"

Paths are configured in .env as SMTP_TLS_CERT_PATH and SMTP_TLS_KEY_PATH.

Configuration

Edit .env file:

USE_MOCK_MSGRAPH=true
MOCK_SIMULATE_FAILURES=0
MSGRAPH_TENANT_ID=your-tenant-id
MSGRAPH_CLIENT_ID=your-client-id
MSGRAPH_CLIENT_SECRET=your-client-secret
SMTP_PORT=2525
SMTP_TLS_CERT_PATH=./certs/smtp.crt
SMTP_TLS_KEY_PATH=./certs/smtp.key
SMTP_USERS={"user1":"user1@example.com","user2":"user2@example.com"}

Configuration Variables

  • USE_MOCK_MSGRAPH: Set to 'true' to use mock client (no Graph API calls). Set to 'false' for production with real Graph API.
  • MOCK_SIMULATE_FAILURES: Number of initial failures to simulate before success (testing retry logic). Only used when USE_MOCK_MSGRAPH=true.
  • MSGRAPH_TENANT_ID: Azure tenant ID. Required if USE_MOCK_MSGRAPH=false.
  • MSGRAPH_CLIENT_ID: App registration client ID. Required if USE_MOCK_MSGRAPH=false.
  • MSGRAPH_CLIENT_SECRET: App registration client secret. Required if USE_MOCK_MSGRAPH=false.
  • SMTP_PORT: Port to listen on (default 2525, non-privileged).
  • SMTP_TLS_CERT_PATH: Path to TLS certificate file.
  • SMTP_TLS_KEY_PATH: Path to TLS private key.
  • SMTP_USERS: JSON object mapping SMTP usernames to mailbox addresses.

User Mapping

User mapping format:

{"username1":"user1@example.com","username2":"user2@example.com"}

When SMTP client authenticates with username1, emails are sent from user1@example.com via MS Graph.

Running

With Mock Client (Testing)

python main.py

Output:

Using Mock MS Graph Client for testing
SMTP Server started on 0.0.0.0:2525

With Real MS Graph

Set in .env:

USE_MOCK_MSGRAPH=false
MSGRAPH_TENANT_ID=12345678-1234-1234-1234-123456789012
MSGRAPH_CLIENT_ID=abcdef01-2345-6789-abcd-ef0123456789
MSGRAPH_CLIENT_SECRET=your_secret_here

Then run:

python main.py

Shutdown

Press Ctrl+C. Server will close connections gracefully.

With mock client enabled, a summary of sent emails is printed:

=== Mock MS Graph Summary ===
Total calls: 3
Emails sent: 3

  Email 1:
    From: user1@example.com
    To: recipient@example.com
    Subject: Test Email
    Body: This is a test...

Usage

Connect via SMTP Client

telnet 127.0.0.1 2525

SMTP session flow:

EHLO hostname
250-EHLO hostname
250-AUTH LOGIN PLAIN
250-STARTTLS
250 OK
STARTTLS
220 Ready to start TLS
AUTH LOGIN
334 VXNlcm5hbWU6
[base64 username]
334 UGFzc3dvcmQ6
[base64 password]
235 2.7.0 Authentication successful
MAIL FROM:<user1@example.com>
250 OK
RCPT TO:<recipient@example.com>
250 OK
DATA
354 Start mail input
[email headers and body]
.
250 OK
QUIT
221 Bye

Send Email via Python

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

smtp = smtplib.SMTP('127.0.0.1', 2525)
smtp.starttls()
smtp.login('user1', 'anypassword')

msg = MIMEMultipart('alternative')
msg['Subject'] = 'Test Email'
msg['From'] = 'user1@example.com'
msg['To'] = 'recipient@example.com'
msg.attach(MIMEText('Email body here', 'plain'))

smtp.sendmail('user1@example.com', ['recipient@example.com'], msg.as_string())
smtp.quit()

Send Email via Command Line

cat << 'EOF' | sendmail -S 127.0.0.1:2525 -au user1 -ap anypassword recipient@example.com
Subject: Test

This is a test email
EOF

Testing

Run Mock Client Tests

Tests mock client email tracking and failure simulation:

python tests/send_smtp_mail.py

Output:

Starting Mock Client Tests

=== Test 1: Mock Client Direct ===
Send result: True - Mock email queued for sending
Emails captured by mock: 1
  Email to: ['recipient@example.com']
  Subject: Test Subject

[... 3 more tests ...]

=== Test Summary ===
PASS: Mock Client Direct
PASS: Mock Client Multiple Recipients
PASS: Mock with Failures
PASS: Mock Query Methods

Total: 4/4 tests passed

Run Integration Tests

Tests configuration loading, mock integration, and server startup:

python tests/integration_test.py

Output:

Starting SMTP Integration Tests

=== Test: Configuration Loading ===
Config loaded successfully:
  - use_mock_msgraph: True
  - mock_simulate_failures: 0
  - smtp_port: 2525
  - smtp_users: 2 user(s)

[... more tests ...]

=== Test Summary ===
PASS: Configuration
PASS: Mock Integration
PASS: Server Startup

Total: 3/3 tests passed

SMTP Client Example

Demonstrates connecting and sending email:

python tests/smtp_client_example.py

Project Structure

msgraph-smtp-adapter/
  adapter/
    __init__.py           - Package exports
    config.py             - Configuration loader
    msgraph/
      __init__.py
      client.py           - Real MS Graph client (OAuth, sendMail)
      mock.py             - Mock client for testing
    smtp/
      __init__.py
      socket.py           - SMTP server (aiosmtpd based)
      handler.py          - SMTP message handler (deprecated)
  tests/
    __init__.py
    send_smtp_mail.py     - Mock client tests
    integration_test.py   - Server and config tests
    smtp_client_example.py - Usage example
  certs/
    smtp.crt              - TLS certificate
    smtp.key              - TLS private key
  main.py                 - Entry point
  requirements.txt        - Python dependencies
  .env                    - Configuration file

Architecture

SMTP Server (aiosmtpd)

Listens on configured port (default 2525). Supports:

  • EHLO with AUTH and STARTTLS capabilities
  • AUTH LOGIN and AUTH PLAIN mechanisms
  • STARTTLS for encryption
  • Email message reception via DATA command

Message Handler

Receives authenticated messages and:

  1. Parses email headers and body
  2. Extracts To, Cc, Bcc recipients
  3. Looks up authenticated user's mailbox
  4. Calls MS Graph client to send

MS Graph Client

Uses MSAL (Microsoft Authentication Library) for Client Credentials OAuth flow:

  1. Acquires access token from Azure AD
  2. Calls MS Graph sendMail API (v1.0)
  3. Implements exponential backoff retry (1s, 2s, 4s)
  4. Handles 401/403 (re-authenticate), 429 (rate limit), 5xx (retry)

Mock Client

Replaces real Graph client for testing:

  1. Tracks all send_email() calls
  2. Simulates configurable failures
  3. Provides query methods (get_sent_emails, get_sent_emails_to, etc.)
  4. Prints summary on shutdown

Error Handling

SMTP Authentication Failures

Incorrect credentials return 535 error:

535 5.7.8 Authentication credentials invalid

User must be in SMTP_USERS mapping.

MS Graph Send Failures

Transient errors (5xx, 429) trigger exponential backoff retry (3 total attempts).

Permanent errors (invalid recipient, quota exceeded) return 550 to client:

550 Error from MS Graph

Client application must handle retry if needed.

Configuration Errors

Missing required environment variables fail at startup:

ValueError: Configuration errors:
MSGRAPH_TENANT_ID is required
MSGRAPH_CLIENT_ID is required
[...]

Troubleshooting

Port Already in Use

Change SMTP_PORT in .env to another non-privileged port (e.g., 2525, 2526).

TLS Certificate Errors

Ensure SMTP_TLS_CERT_PATH and SMTP_TLS_KEY_PATH point to valid files:

ls -la certs/smtp.crt certs/smtp.key

Regenerate if needed:

rm certs/smtp.* && openssl req -x509 -newkey rsa:2048 -keyout certs/smtp.key \
  -out certs/smtp.crt -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Org/CN=localhost"

STARTTLS Not Working

Clients must support STARTTLS. Server advertises capability in EHLO response.

Some SMTP libraries require explicit flag:

smtp.starttls()  # Must call before login()

Authentication Failures

Verify SMTP_USERS JSON is valid:

python -c "import json; json.loads('{\"user1\":\"user1@example.com\"}')"

Username must match exactly (case-sensitive).

Dependencies

  • aiosmtpd (1.4.5) - Async SMTP server framework
  • aiohttp (3.9.5) - Async HTTP client for Graph API
  • aiofiles (25.1.0) - Async file operations
  • msal (1.28.0) - Microsoft Authentication Library
  • python-dotenv (1.0.1) - .env file support
  • email-validator (2.1.1) - Email validation

See requirements.txt for exact versions.

Performance Notes

  • Single-threaded async event loop (aiosmtpd)
  • Handles concurrent SMTP connections via asyncio
  • MS Graph requests are async (aiohttp)
  • No queue persistence (in-memory only)
  • Exponential backoff prevents Graph API hammering

Expected throughput: Hundreds of concurrent connections on modern hardware.

Production Deployment

For production use:

  1. Switch from mock to real MS Graph:

    USE_MOCK_MSGRAPH=false
    
  2. Register application in Azure AD with Mail.Send permission

  3. Use CA-signed TLS certificate (not self-signed)

  4. Run behind reverse proxy (e.g., HAProxy, Nginx) with:

    • Connection limits
    • Rate limiting
    • TLS termination (optional)
    • Logging
  5. Monitor:

    • SMTP connection errors
    • Graph API rate limiting (429 responses)
    • Failed authentication attempts
  6. Logging: Currently prints to stdout/stderr. For production, integrate with syslog or centralized logging.

License

MIT

Support

For issues or questions, check:

  1. Configuration in .env
  2. Test results: python tests/send_smtp_mail.py
  3. Server logs on startup and shutdown

About

MS-Graph SMTP Adapter

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages