Parsing Mandrill Inbound Webhooks: A Minimal Python Wrapper Worth Understanding
Hook
Most developers overthink webhook parsing, reaching for heavy email libraries when they really just need clean access to a JSON payload. This 200-line library shows how simple it should be.
Context
When Mandrill launched inbound email routing in the early 2010s, it filled a crucial gap for developers who needed to receive and process emails programmatically. The use cases were everywhere: support ticket systems that created issues from emails, marketing platforms that tracked replies, IoT devices that accepted commands via email, and CRM systems that logged correspondence automatically.
But Mandrill's webhook format was deeply nested JSON, requiring developers to navigate multi-level dictionaries just to extract basic information like the sender's email or attachment count. Every Flask or Django webhook endpoint ended up with the same ugly code: data['msg']['from_email'], data['msg']['subject'], repeated endlessly. Jose Padilla built mandrill-inbound-python to solve this ergonomics problem—not by adding features, but by providing a clean interface to data you already have. It's the same philosophy behind his postmark-inbound-python library, creating consistency across different email service providers.
Technical Insight
The architecture is deliberately minimal: a single MandrillInbound class that wraps the webhook payload and exposes properties for everything you'd want to extract. When Mandrill POSTs to your webhook endpoint, you pass the raw data to the wrapper and immediately get cleaner access.
Here's what the difference looks like in practice:
from flask import Flask, request
from mandrill_inbound import MandrillInbound
app = Flask(__name__)
@app.route('/webhook/mandrill', methods=['POST'])
def handle_inbound():
# Mandrill sends form-encoded data with 'mandrill_events' key
mandrill_events = request.form.get('mandrill_events')
# The wrapper accepts either JSON string or dict
inbound = MandrillInbound(json=mandrill_events)
# Clean property access instead of nested dict navigation
sender = inbound.sender_email() # vs data['msg']['from_email']
subject = inbound.subject() # vs data['msg']['subject']
body = inbound.text_body() # vs data['msg']['text']
# Check spam indicators before processing
if inbound.spf_pass() and inbound.dkim_valid():
# Safe to process
create_support_ticket(sender, subject, body)
else:
# Log suspicious email
log_spam_attempt(sender, inbound.spam_score())
return 'OK', 200
The library shines with attachment handling. Instead of parsing the attachments array manually, you get utility methods that filter by content type and handle downloads:
# Get only image attachments
for attachment in inbound.attachments('image/*'):
filename = attachment.get('name')
base64_content = attachment.get('content')
# Or download directly if you have the attachment URL
inbound.download_attachment(attachment, save_path='/uploads/')
# Common pattern: save PDFs from invoice emails
pdf_attachments = inbound.attachments('application/pdf')
for pdf in pdf_attachments:
inbound.download_attachment(pdf, '/invoices/')
Under the hood, the implementation is straightforward property methods that traverse the nested structure. The sender_email() method, for example, simply returns self.source.get('msg', {}).get('from_email'). This might seem trivial, but it provides two key benefits: it handles missing keys gracefully (returning None instead of raising KeyError), and it documents what fields are actually available in Mandrill's payload.
The spam detection helpers are particularly valuable for production systems. Email abuse is rampant, and Mandrill includes SPF, DKIM, and spam score data in every webhook. The library surfaces these as boolean methods:
def is_legitimate_email(inbound):
# SPF verifies the sending server is authorized
spf_valid = inbound.spf_pass()
# DKIM verifies the message hasn't been tampered with
dkim_valid = inbound.dkim_valid()
# Spam score from Mandrill's filters (lower is better)
spam_score = inbound.spam_score()
return spf_valid and dkim_valid and spam_score < 5.0
One subtle design choice: the library doesn't validate or transform the incoming data. It assumes Mandrill's webhook format is correct and simply provides accessors. This keeps the code simple but means you need error handling around the webhook endpoint itself. If Mandrill sends malformed JSON or your endpoint receives garbage data, you'll get exceptions from the JSON parser, not from MandrillInbound.
Gotcha
The biggest limitation is maintenance status. This library hasn't been updated since 2016, which isn't necessarily a death sentence for simple wrapper code, but it means no modern Python features. There are no type hints, so you lose IDE autocomplete benefits and can't use mypy for type checking. There's no async/await support, which matters if you're building on FastAPI or modern async frameworks. The test suite uses an outdated Travis CI configuration, and dependencies are pinned to old versions.
More concerning is the lack of error handling documentation. What happens if Mandrill changes their webhook format? What if the JSON is malformed? What if an attachment URL is invalid during download? The library doesn't answer these questions, leaving you to discover edge cases in production. The download_attachment method, for instance, uses Python's urllib to fetch files but doesn't handle network timeouts, HTTP errors, or filesystem permissions issues. You'll need to wrap these calls in your own try-except blocks.
The attachment filtering by content type is also somewhat naive—it uses simple string matching with wildcards, which works for common cases like 'image/*' but doesn't handle MIME type parameters or edge cases properly. If you need robust attachment processing, you'd want something like the email module from Python's standard library or a more comprehensive parser like flanker.
Verdict
Use if: You're building a simple webhook endpoint for Mandrill inbound emails and want to avoid manual dictionary traversal. The library provides immediate value for straightforward use cases—support ticket creation, email-to-SMS forwarding, simple autoresponders—where you just need to extract sender, subject, and body. It's also useful as reference code if you're building a similar wrapper for another email service. Skip if: You need production-grade reliability, type safety, or active maintenance. Given the library's age and simplicity (under 200 lines), you're better off writing your own thin wrapper using modern Python 3.8+ features with proper type hints and async support. Also skip if you're processing complex MIME structures, need comprehensive attachment validation, or require detailed error handling—at that point, use a full email parsing library like Python's built-in email.parser module or Mailgun's flanker library.