SMTP server that relays received emails to Microsoft Graph API for sending. Supports multiple user-to-mailbox mappings, STARTTLS encryption, and basic SMTP authentication.
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
- 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
cd msgraph-smtp-adapter
python3 -m venv .venvLinux/macOS:
source .venv/bin/activateWindows:
.venv\Scripts\activatepip install -r requirements.txtFor 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.
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"}
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 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.
python main.pyOutput:
Using Mock MS Graph Client for testing
SMTP Server started on 0.0.0.0:2525
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.pyPress 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...
telnet 127.0.0.1 2525SMTP 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
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()cat << 'EOF' | sendmail -S 127.0.0.1:2525 -au user1 -ap anypassword recipient@example.com
Subject: Test
This is a test email
EOFTests mock client email tracking and failure simulation:
python tests/send_smtp_mail.pyOutput:
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
Tests configuration loading, mock integration, and server startup:
python tests/integration_test.pyOutput:
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
Demonstrates connecting and sending email:
python tests/smtp_client_example.pymsgraph-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
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
Receives authenticated messages and:
- Parses email headers and body
- Extracts To, Cc, Bcc recipients
- Looks up authenticated user's mailbox
- Calls MS Graph client to send
Uses MSAL (Microsoft Authentication Library) for Client Credentials OAuth flow:
- Acquires access token from Azure AD
- Calls MS Graph sendMail API (v1.0)
- Implements exponential backoff retry (1s, 2s, 4s)
- Handles 401/403 (re-authenticate), 429 (rate limit), 5xx (retry)
Replaces real Graph client for testing:
- Tracks all send_email() calls
- Simulates configurable failures
- Provides query methods (get_sent_emails, get_sent_emails_to, etc.)
- Prints summary on shutdown
Incorrect credentials return 535 error:
535 5.7.8 Authentication credentials invalid
User must be in SMTP_USERS mapping.
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.
Missing required environment variables fail at startup:
ValueError: Configuration errors:
MSGRAPH_TENANT_ID is required
MSGRAPH_CLIENT_ID is required
[...]
Change SMTP_PORT in .env to another non-privileged port (e.g., 2525, 2526).
Ensure SMTP_TLS_CERT_PATH and SMTP_TLS_KEY_PATH point to valid files:
ls -la certs/smtp.crt certs/smtp.keyRegenerate 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"Clients must support STARTTLS. Server advertises capability in EHLO response.
Some SMTP libraries require explicit flag:
smtp.starttls() # Must call before login()Verify SMTP_USERS JSON is valid:
python -c "import json; json.loads('{\"user1\":\"user1@example.com\"}')"Username must match exactly (case-sensitive).
- 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.
- 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.
For production use:
-
Switch from mock to real MS Graph:
USE_MOCK_MSGRAPH=false -
Register application in Azure AD with Mail.Send permission
-
Use CA-signed TLS certificate (not self-signed)
-
Run behind reverse proxy (e.g., HAProxy, Nginx) with:
- Connection limits
- Rate limiting
- TLS termination (optional)
- Logging
-
Monitor:
- SMTP connection errors
- Graph API rate limiting (429 responses)
- Failed authentication attempts
-
Logging: Currently prints to stdout/stderr. For production, integrate with syslog or centralized logging.
MIT
For issues or questions, check:
- Configuration in .env
- Test results: python tests/send_smtp_mail.py
- Server logs on startup and shutdown