Skip to content

Instantly share code, notes, and snippets.

@itsnotyoutoday
Created August 18, 2025 01:19
Show Gist options
  • Save itsnotyoutoday/e45ebbde8347c54e0c97f597c64381de to your computer and use it in GitHub Desktop.
Save itsnotyoutoday/e45ebbde8347c54e0c97f597c64381de to your computer and use it in GitHub Desktop.
Mailjet Postfix Transport
#!/usr/bin/env python3
"""
Mailjet Email Relay Script
==========================
Background, I had some issues working with mailjet's smtp relay. However, I found
their API to be reliable. So here's my custom python script acts as a
Postfix transport to relay emails via Mailjet API.
Setup Instructions:
1. Install required Python package: pip3 install requests
2. Set your Mailjet API credentials as environment variables:
export MAILJET_API_KEY="your_api_key_here"
export MAILJET_API_SECRET="your_api_secret_here"
3. Set default sender domain (optional):
export MAILJET_DEFAULT_DOMAIN="yourdomain.com"
4. Make script executable: chmod +x mailjet-relay.py
Postfix Integration:
1. Add to /etc/postfix/master.cf:
mailjet-api unix - n n - - pipe
flags=FR user=postfix argv=/path/to/mailjet-relay.py ${recipient}
2. Add to transport maps (e.g., /etc/postfix/transport):
@yourdomain.com mailjet-api:
3. Update Postfix config:
postmap /etc/postfix/transport
postfix reload
Usage:
- Script reads email from stdin and recipient from command line argument
- Supports plain text, HTML, and attachments
- Uses Mailjet API v3.1 for sending
Environment Variables:
- MAILJET_API_KEY: Your Mailjet API key (required)
- MAILJET_API_SECRET: Your Mailjet API secret (required)
- MAILJET_DEFAULT_DOMAIN: Default domain for fallback sender (default: localhost)
- MAILJET_DEFAULT_USER: Default username for fallback sender (default: postmaster)
By: Rick L Bird
License: MIT
"""
import sys
import json
import base64
import requests
import os
from email import message_from_string
from email.utils import parseaddr
# Mailjet API configuration
API_KEY = os.environ.get('MAILJET_API_KEY')
API_SECRET = os.environ.get('MAILJET_API_SECRET')
API_URL = "https://api.mailjet.com/v3.1/send"
# Default sender configuration
DEFAULT_DOMAIN = os.environ.get('MAILJET_DEFAULT_DOMAIN', 'localhost')
DEFAULT_USER = os.environ.get('MAILJET_DEFAULT_USER', 'postmaster')
DEFAULT_SENDER = f"{DEFAULT_USER}@{DEFAULT_DOMAIN}"
def send_via_mailjet():
# Check for required credentials
if not API_KEY or not API_SECRET:
print("Error: MAILJET_API_KEY and MAILJET_API_SECRET environment variables must be set", file=sys.stderr)
sys.exit(74) # Configuration error
# Get recipient from command line argument
recipient_email = sys.argv[1] if len(sys.argv) > 1 else None
# Read the email from stdin
raw_email = sys.stdin.buffer.read()
# Parse the email
from email.parser import BytesParser
from email.policy import default
msg = BytesParser(policy=default).parsebytes(raw_email)
# Extract headers
from_addr = parseaddr(msg.get('From', DEFAULT_SENDER))
to_header = msg.get('To', '')
cc_header = msg.get('Cc', '')
bcc_header = msg.get('Bcc', '')
subject = msg.get('Subject', 'No Subject')
# Use command line recipient if no To header
to_addrs = []
if recipient_email:
to_addrs = [('', recipient_email)]
elif to_header:
to_addrs = [parseaddr(addr.strip()) for addr in to_header.split(',') if addr.strip()]
cc_addrs = []
if cc_header:
cc_addrs = [parseaddr(addr.strip()) for addr in cc_header.split(',') if addr.strip()]
bcc_addrs = []
if bcc_header:
bcc_addrs = [parseaddr(addr.strip()) for addr in bcc_header.split(',') if addr.strip()]
# Get message body and attachments
body = ""
html_body = ""
attachments = []
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get('Content-Disposition', ''))
if content_type == "text/plain" and 'attachment' not in content_disposition:
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
elif content_type == "text/html" and 'attachment' not in content_disposition:
html_body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
# Remove any escaping that might cause issues with DOCTYPE
html_body = html_body.replace('<\\!', '<!')
elif 'attachment' in content_disposition or part.get_filename():
# Handle attachment
filename = part.get_filename()
if filename:
attachment_data = part.get_payload(decode=True)
if attachment_data:
attachments.append({
"ContentType": content_type,
"Filename": filename,
"Base64Content": base64.b64encode(attachment_data).decode('utf-8')
})
else:
body = msg.get_payload(decode=True).decode('utf-8', errors='ignore')
# If no HTML body, use plain text
if not html_body:
html_body = f"<pre>{body}</pre>"
# Build Mailjet API payload
message_data = {
"From": {
"Email": from_addr[1] if from_addr[1] else DEFAULT_SENDER,
"Name": from_addr[0] if from_addr[0] else "Mail System"
},
"To": [
{
"Email": to_addr[1],
"Name": to_addr[0] if to_addr[0] else to_addr[1]
} for to_addr in to_addrs if to_addr[1]
],
"Subject": subject,
"TextPart": body,
"HTMLPart": html_body
}
# Add CC if any
if cc_addrs:
message_data["Cc"] = [
{
"Email": cc_addr[1],
"Name": cc_addr[0] if cc_addr[0] else cc_addr[1]
} for cc_addr in cc_addrs if cc_addr[1]
]
# Add BCC if any
if bcc_addrs:
message_data["Bcc"] = [
{
"Email": bcc_addr[1],
"Name": bcc_addr[0] if bcc_addr[0] else bcc_addr[1]
} for bcc_addr in bcc_addrs if bcc_addr[1]
]
# Add attachments if any
if attachments:
message_data["Attachments"] = attachments
data = {"Messages": [message_data]}
# Send via Mailjet API
try:
response = requests.post(
API_URL,
auth=(API_KEY, API_SECRET),
headers={'Content-Type': 'application/json'},
json=data
)
if response.status_code == 200:
result = response.json()
if result.get('Messages', [{}])[0].get('Status') == 'success':
sys.exit(0) # Success
else:
print(f"Mailjet API error: {result}", file=sys.stderr)
sys.exit(75) # Temporary failure
else:
print(f"HTTP {response.status_code}: {response.text}", file=sys.stderr)
sys.exit(75) # Temporary failure
except Exception as e:
print(f"Error sending via Mailjet API: {e}", file=sys.stderr)
sys.exit(75) # Temporary failure
if __name__ == "__main__":
send_via_mailjet()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment