SendGrid Inbound Parse Webhook: Receive & Process Incoming Email
In this tutorial, you will learn about SendGrid Inbound Parse Webhook: Receive & Process Incoming Email. We cover key concepts, practical examples, and best practices to help you master this topic.
SendGrid Inbound Parse Webhook receives incoming emails sent to your domain, parses them into structured data (headers, body, attachments), and POSTs to your endpoint.
What You'll Learn
How to configure the Inbound Parse Webhook, receive and parse incoming email, extract attachments, handle reply-to workflows, and build email-to-ticket systems.
Why It Matters
Processing incoming email enables powerful workflows: support@ becomes a ticket system, replies auto-attach to conversations. DodaTech uses inbound Parsing so users can reply to threat alerts and their responses are logged to the incident.
Real-World Use
A user replies to a threat alert email. SendGrid parses the reply, POSTs to the webhook, which logs the response as a comment on the security incident.
flowchart LR
A["User Replies\nto Alert Email"] --> B["MX Record\nPoints to SendGrid"]
B --> C["SendGrid\nParse Email"]
C --> D["POST to\nYour Webhook"]
D --> E["Extract\n+ Parse"]
E --> F["Store Reply\non Incident"]
E --> G["Auto-Reply\nto User"]
style A fill:#dbeafe,stroke:#2563eb
style C fill:#1a82e2,color:#fff
style D fill:#fef3c7,stroke:#d97706
Configuring Inbound Parse
# 1. Add MX record in DNS:
# Type: MX
# Name: inbound.yourdomain.com
# Value: mx.sendgrid.net
# Priority: 10
# 2. In SendGrid Dashboard:
# Settings > Inbound Parse
# Hostname: inbound.yourdomain.com
# URL: https://api.dodatech.com/sendgrid/inbound
# Spam check: On
# Send raw: Off
Receiving Parsed Email
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route("/sendgrid/inbound", methods=["POST"])
def handle_inbound():
# Extract fields from multipart form data
to_email = request.form.get("to")
from_email = request.form.get("from")
subject = request.form.get("subject")
text_body = request.form.get("text")
html_body = request.form.get("html")
attachments_count = int(request.form.get("attachments", 0))
print(f"Email received from {from_email}")
print(f"Subject: {subject}")
print(f"Body: {text_body[:200]}...")
# Process attachments
for i in range(attachments_count):
attachment = request.files.get(f"attachment{i}")
if attachment:
filename = attachment.filename
content_type = attachment.content_type
print(f" Attachment {i}: {filename} ({content_type})")
# Store in database
# db.incoming_emails.insert_one({
# "from": from_email,
# "subject": subject,
# "body": text_body,
# "received_at": datetime.utcnow()
# })
return jsonify({"status": "ok"}), 200
Parsing Reply Threads
import re
def parse_reply(text_body):
# Remove original message (common email clients)
lines = text_body.split("\n")
reply_lines = []
for line in lines:
# Stop at common reply markers
if re.match(r"^On .+ wrote:", line.strip()):
break
if re.match(r"^_{10,}", line.strip()):
break
if line.strip().startswith("> "):
continue
reply_lines.append(line)
reply_text = "\n".join(reply_lines).strip()
return reply_text
@app.route("/sendgrid/inbound", methods=["POST"])
def handle_reply():
from_email = request.form.get("from")
text_body = request.form.get("text", "")
subject = request.form.get("subject", "")
references = request.form.get("references", "")
reply_text = parse_reply(text_body)
print(f"Parsed reply from {from_email}: {reply_text[:100]}...")
# Extract incident ID from subject or references
incident_id = extract_incident_id(subject, references)
if incident_id:
add_reply_to_incident(incident_id, from_email, reply_text)
print(f"Reply added to incident: {incident_id}")
else:
create_new_ticket(from_email, reply_text, subject)
print(f"New ticket created from email reply")
return jsonify({"status": "ok"}), 200
def extract_incident_id(subject, references):
match = re.search(r"\[INC-(\d+)\]", subject)
if match:
return match.group(1)
# Fallback: check references header for message IDs
return None
Processing Attachments
import os
from werkzeug.utils import secure_filename
UPLOAD_DIR = "/tmp/email_attachments"
def process_attachments(request, incident_id):
attachments_count = int(request.form.get("attachments", 0))
saved_files = []
for i in range(attachments_count):
attachment = request.files.get(f"attachment{i}")
if attachment and attachment.filename:
filename = secure_filename(f"{incident_id}_{int(time.time())}_{attachment.filename}")
filepath = os.path.join(UPLOAD_DIR, filename)
attachment.save(filepath)
saved_files.append({
"original_name": attachment.filename,
"saved_as": filename,
"content_type": attachment.content_type,
"size": os.path.getsize(filepath)
})
print(f"Saved attachment: {filename} ({attachment.content_type})")
return saved_files
Common Mistakes
1. DNS Misconfiguration
The MX record must point to mx.sendgrid.net with priority 10. Incorrect DNS means emails never reach SendGrid for parsing.
2. Not Handling Multipart Form Data
Inbound Parse sends multipart/form-data, not JSON. Use request.form for fields and request.files for attachments.
3. Ignoring Spam Check Results
Enable spam checking and check spam_score and spam_report fields. Process spam emails differently or discard them.
4. Processing Attachments Synchronously
Attachments can be large. Save them asynchronously or stream to cloud storage to avoid webhook timeout.
5. Not Handling Email Encoding
Emails may have different character encodings (UTF-8, ISO-8859-1). Handle encoding properly to avoid garbled text.
Practice Questions
- How do you configure DNS for Inbound Parse?
- What data format does the webhook use?
- How do you extract the sender's email address?
- How do you handle email attachments?
Answers:
- Add an MX record: hostname =
inbound.yourdomain.com, value =mx.sendgrid.net, priority = 10. - The webhook sends multipart/form-data with form fields (to, from, subject, text, html, etc.) and file fields (attachments).
- The
fromfield in the form data contains the sender's email address as a string. - Attachments arrive as file fields (attachment0, attachment1, etc.). Access via
request.files.get("attachment0")and save or process immediately.
Challenge: Build an email-to-ticket system: configure Inbound Parse, parse incoming emails into tickets or replies, handle attachments (store in cloud storage), detect reply-vs-new-email, and store the parsed data in a database with full-text search.
FAQ
Mini Project
Build an email-to-incident system: configure Inbound Parse with MX record, parse incoming emails into structured data, detect replies vs new emails (using subject/References header), save attachments to cloud storage, and store parsed emails in a database searchable by sender and subject.
What's Next
SendGrid v3 API Deep Dive — explore the full REST API for advanced operations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro