Most applications start by sending email, and then someone asks for the other direction. Customers reply to a notification and expect the reply to land in the ticket. Team members want to answer a comment by email. A support address needs to turn into a queue. This is inbound email: receiving messages and turning them into data your application can act on. It is not hard, but it has more sharp edges than sending, because you are now accepting content from anyone on the internet. This guide walks through how inbound mail reaches your app, how to parse and route it, and how to keep the whole thing safe and repeatable.
A note on scope. The mechanics below are how email works in general. The exact inbound options, payload format and limits differ between platforms, so check your MigoSMTP dashboard or API reference for the inbound options and payload format available to you before you design around them.
Why inbound email matters
- Support replies. A customer answers a message from your system and the answer should join the same conversation.
- Reply-by-email. Users respond to a comment or task notification from their mail app, and the text is posted back into your product.
- Ticketing. Mail sent to a support address becomes a ticket, with attachments, sender and thread attached.
- Forwarding and capture. Users forward receipts, invoices or leads to a special address for your app to read.
- Feedback signals. Some replies are unsubscribe requests or complaints written in plain words, and you will want to see them.
How inbound mail reaches your app
When someone sends a message to an address at your inbound domain, their mail server looks up the MX records for that domain in DNS. The MX records name the servers that accept mail for the domain. The sending server connects to one of them over SMTP and hands the message over. What happens next depends on the model you choose.
| Model | How it works | Good for | Watch out for |
|---|---|---|---|
| Webhook (HTTP POST) | The receiving service accepts the SMTP message and posts it, or a parsed version of it, to your endpoint | Real-time workflows, reply-by-email, ticketing | Your endpoint must be fast, authenticated and safe to call twice |
| Mailbox polling (IMAP or POP) | Your app logs in to a mailbox and fetches new messages on a schedule | Low volume, simple setups, shared support inboxes | Delay between polls, credential handling, state tracking |
| Direct SMTP receiver you run | You operate your own server that accepts mail | Special cases with full control | You own spam filtering, TLS, queues and abuse handling |
| Forward to an address, then parse | Mail is forwarded from an existing mailbox into one of the models above | Migrating a support address gradually | Forwarding can break authentication results and alter headers |
Set up the receiving side
- Choose the subdomain. For example reply.example.com. Use it only for inbound.
- Publish MX records for that subdomain, pointing at the hostnames your receiving service gives you. Use exactly the values from the service, with the priorities it specifies.
- Register the address or domain with the service and tell it where to deliver: the URL of your endpoint, or the mailbox to store into.
- Check delivery by sending a message from an outside account and confirming that it arrives in your app. Allow time for DNS changes to spread, as described in how to verify your sending domain.
Keep inbound on its own subdomain. MX records apply to the whole name. If you point the MX of your main domain at your app, staff mail may stop arriving. A dedicated inbound subdomain avoids that, and it keeps unsolicited mail and spam aimed at your inbound address away from the domain and reputation you use to send. Keep your outbound records, covered in authenticating a sending domain, on their own names too.
Parsing a message
An email is a set of headers followed by a body, defined in RFC 5322. Modern messages use MIME (RFC 2045 and the documents around it) to carry several parts in one message. Parsing it yourself with string splitting is a common source of bugs. Use a well-tested MIME library, or the parsed output of your receiving service, and still check the details below.
Structure to expect
- Single part or multipart. A simple message is one text part. Most real mail is multipart: multipart/alternative holds a text/plain and a text/html version of the same content, and multipart/mixed adds attachments.
- Text versus HTML. Prefer the plain text part for logic such as reply extraction. If you must use HTML, sanitise it before display, because it can hold scripts, tracking images and hostile links.
- Character sets and encodings. Each part declares a charset and a transfer encoding such as base64 or quoted-printable. Decode both before reading the text. Headers with non-ASCII text use encoded words, defined in RFC 2047.
- Attachments. They arrive as parts with a filename and a content type. Both are claimed by the sender and can be false. Never trust the extension or the declared type.
- Nested messages. A forwarded email may be attached as message/rfc822. Bounce reports are multipart/report. Decide whether you want to descend into them.
- Odd and malformed mail. Missing headers, duplicate headers and invalid encodings happen. Your parser should fail safely, store the raw message and move on.
Keep the raw message, or a reference to it, alongside the parsed fields for as long as your retention policy allows. When parsing goes wrong, the raw source is the only way to find out why. Our guide to reading email headers helps with inspecting it, and email size limits, attachments and MIME covers the building side.
Threading and routing replies
The hard part of reply-by-email is knowing which record a reply belongs to. There are two complementary methods, and robust systems use both.
Method 1: headers
Every message should carry a unique Message-ID. When a mail client replies, it normally sets In-Reply-To to the Message-ID of the message being answered, and References to the chain of earlier IDs (RFC 5322). If you store the Message-ID of every notification you send and the record it relates to, you can look up the reply by those headers. The weakness is that clients, forwarding systems and some gateways drop or rewrite these headers.
Method 2: reply tokens in the address
Set a Reply-To header on your outbound mail that points at your inbound subdomain and includes a token that identifies the record. The common forms are plus-addressing, such as reply+TOKEN@reply.example.com, or a unique local part. Plus-addressing is a widely used convention rather than a universal standard, so confirm that your receiving service handles it, or use a wildcard or a separator it supports.
- Make the token opaque: a random identifier looked up in your database, or a value signed with a secret key. Never use a plain sequential ID that someone could guess.
- Tie the token to the expected sender where possible, so a forwarded or leaked address cannot be used by a stranger.
- Give tokens an expiry if the action is sensitive, and decide what happens to replies that arrive after it.
- Keep the From address on your main sending domain, authenticated as usual, and use Reply-To only for routing.
This is close to how VERP-style return addressing works for bounces, where the recipient is encoded into the return path. The idea is the same: put the identifier in the address, and the machine can read it back without parsing the text. For bounce handling, see SMTP bounce codes explained.
Stripping quoted text
A reply usually repeats the earlier message below the new text. If you post the whole body as a comment, the thread fills with duplicates. There is no standard marker for where the new text ends. Typical signals are lines that start with a greater-than sign, a line such as On some date, someone wrote, or a separator line from the mail client. Use a maintained library and your own tests on real samples, and always store the full original body so that nothing is lost if the heuristic cuts too much. Also strip signatures and disclaimers only if you can do so safely.
Auto-replies, bounces and real replies
A large share of what arrives at an inbound address is not a person writing back. If your system treats every message as a human reply, it can post out-of-office notices into customer tickets or, worse, answer a machine and start a loop.
| Message type | Signals | What to do |
|---|---|---|
| Human reply | Has In-Reply-To or a valid token; no automatic markers; Auto-Submitted absent or no | Process and route |
| Out-of-office or auto-reply | Auto-Submitted: auto-replied (RFC 3834); often an empty return path; subjects such as Automatic reply | Log it, do not reply, do not count as engagement |
| Bounce or delivery report | Empty return path (null sender); multipart/report content (RFC 3464); sender such as a mail system account | Send to bounce handling, not to the conversation |
| Bulk or list mail | Headers such as Precedence: bulk or List-Id, which are common but informal | Usually ignore, or hold for review |
| Unauthenticated or suspicious | SPF, DKIM and DMARC fail or are absent; mismatch between From and signing domain | Quarantine or limit what the message can trigger |
Per RFC 3834, a system that sends an automatic response should mark it with the Auto-Submitted header and should not respond to messages that are themselves marked as automatic. Follow the same rule yourself: never auto-reply to a message with Auto-Submitted other than no, and never send anything to an empty return path. Real-world auto-responders do not always follow the rules, so add a rate limit per sender as a backstop.
Security: spam, malware and forged senders
An inbound address is a public door. Once it is known, assume it will receive spam, phishing and files you did not expect.
- Scan attachments for malware before anyone can open them, and store them outside your web root with no execute permission.
- Do not render HTML mail raw in your app. Sanitise it, block remote content by default and strip scripts.
- Authenticate your own webhook. If the platform delivers by HTTP POST, verify the signature or shared secret on each call, as in email webhooks and signature verification. Otherwise anyone can post a fake message to your endpoint.
- Read the authentication results. The receiving server records SPF, DKIM and DMARC outcomes, commonly in an Authentication-Results header (RFC 8601). Trust only the results added by your own receiving service, not a header supplied by the sender. Because From is easily forged, do not let an unverified From address approve payments, change settings or reveal data.
- Limit what an email can do. Treat the content as data. Do not run commands, follow links automatically or expand templates from message text.
- Expect abuse of the reply path. A flood to one address can fill queues and storage, so apply size limits and rate limits.
Reliable processing
Inbound delivery is at-least-once in practice: if your endpoint is slow, restarts or returns an error, the sender will try again, and you may see the same message twice. The principles are the same as for outbound events, described in handling webhooks reliably.
# Pseudocode for an inbound handler
function handle_inbound(request):
verify_signature(request) # reject forgeries
raw = request.body
if size(raw) > MAX_BYTES: return reject_too_large()
msg_id = header(raw, Message-ID) or hash(raw)
if already_processed(msg_id): return ok() # idempotent
store_raw(msg_id, raw) # durable first
enqueue(process_message, msg_id) # work happens later
return ok() # acknowledge fast
function process_message(msg_id):
msg = parse(load_raw(msg_id))
if is_auto_reply(msg) or is_bounce(msg): route_elsewhere(msg); return
if not authenticated(msg): quarantine(msg); return
record = find_by_token(msg.to) or find_by_headers(msg)
if not record: park_for_review(msg); return
add_reply(record, strip_quoted(msg.text))
- Store first, work later. Save the raw message and acknowledge quickly. Heavy parsing and scanning happen in a background job.
- Be idempotent. Key on Message-ID, or on a delivery identifier from your platform if it provides one, and make a second run harmless.
- Set size limits. Servers advertise a maximum message size in SMTP (RFC 1870), and your receiving service and your own endpoint have limits too. Decide what happens to oversized mail and tell senders clearly. See your MigoSMTP dashboard or API reference for the limits that apply to you.
- Handle unmatched mail. Park messages that match no record in a review queue instead of dropping them.
- Plan for retries and outages. If your app is down, the sending side will retry for a while and then give up, and the original sender may get a bounce. Keep the endpoint simple and highly available.
Privacy and retention
Inbound messages contain personal data: names, addresses, signatures and often the full text of earlier conversations. You are responsible for it once it reaches you. Decide up front what you keep and for how long, give only the people who need it access, and delete raw messages and attachments when they are no longer needed. Avoid logging full bodies in application logs. If you serve people in regions with data protection laws, include inbound mail in your records, as discussed in GDPR and email. We do not state specific retention periods here, because the right one depends on your purpose and your legal advice.
A worked example: replies to task notifications
A project tool sends a notification each time someone is assigned a task. The team wants people to answer from their inbox and have the answer appear as a comment on the task.
They create the subdomain reply.example.com, publish the MX records the receiving service gives them, and configure it to POST each message to a signed endpoint. Each notification is sent from the main authenticated domain with a Reply-To such as reply+TOKEN@reply.example.com, where the token is a random value stored against the task and the recipient. The Message-ID of the notification is also saved.
When a reply arrives, the endpoint verifies the signature, stores the raw message and acknowledges. A background job then parses it, prefers the plain text part, and checks that it is not an auto-reply. It looks up the token, confirms the sender matches the person the token was issued to, strips the quoted block and posts the rest as a comment. An out-of-office message is logged and ignored. A reply with an unknown token goes to a review queue. If the receiving service delivers the same message twice, the second call finds the stored Message-ID and does nothing. The scenario is illustrative, but each decision answers a real failure mode described above.
Testing inbound email
- Send real messages from several clients and providers, including mobile apps, and compare the parsed results. Quoted-text formats differ between them.
- Collect a folder of sample raw messages: plain text, HTML only, multipart with attachments, non-ASCII text, an out-of-office reply and a bounce. Replay them in automated tests.
- Test duplicates, very large messages, missing headers and bad encodings.
- Test the failure of your own endpoint, and confirm that messages are retried and not lost.
- Use a staging inbound subdomain with its own credentials, following testing email safely, so tests never touch production data.
A short checklist
- Inbound uses its own subdomain with MX records set exactly as your receiving service specifies.
- The endpoint verifies the signature of every delivery and acknowledges quickly.
- The raw message is stored before processing, and processing is idempotent.
- Parsing uses a tested MIME library and handles charsets, encodings and malformed mail.
- Replies are matched by an opaque reply token and by In-Reply-To and References.
- Auto-replies, bounces and bulk mail are detected and never answered.
- SPF, DKIM and DMARC results are checked before sensitive actions.
- Attachments are scanned, size limits are set and a retention period is defined.
Planning to receive email in your app? Tell us what you want to do with incoming messages and we will point you to the inbound options available on your account.
Further reading and sources
Frequently asked questions
Why should inbound mail use a different subdomain from outbound?
MX records decide where mail for a domain is delivered, so pointing them at your app can disturb the mailboxes people already use on your main domain. A separate subdomain keeps receiving simple and isolated, and keeps your sending reputation separate from whatever strangers send to your inbound address.
Can I receive replies at the same address I send from?
You can, but it is usually better to set a Reply-To header that points to a dedicated inbound address. The From address stays recognisable and authenticated, while replies go to the address your application reads.
What is plus-addressing and is it reliable?
It is a convention where the part after a plus sign in the local part, such as reply+token@example.com, is treated as a tag for the same mailbox. It is not universal, so check that your receiving service accepts it. Many services also let you use a wildcard or a custom separator for the same purpose.
How do I match a reply to the original conversation?
Use a reply token in the address, or compare the In-Reply-To and References headers with the Message-ID values of messages you sent. Tokens are more reliable because some clients drop or rewrite headers. Using both gives the best coverage.
How do I strip quoted text from a reply?
There is no standard marker, so use a maintained parsing library and a few heuristics, such as lines starting with a greater-than sign or an On date, someone wrote line. Always keep the full original body as well, because stripping can remove real content.
How should I handle out-of-office replies?
Check for an Auto-Submitted header with a value other than no, as defined in RFC 3834, and for an empty return path. Do not reply to these and do not treat them as human answers. Never send an automatic response to an automatic response.
Should I trust the From address of inbound mail?
Not on its own. The From header is easy to forge. Check SPF, DKIM and DMARC results recorded by the receiving side, and for sensitive actions use a signed reply token as well, so that a guessed address is not enough.
Why can the same message be delivered to my app twice?
Delivery from a receiving service to your endpoint is normally retried when your app is slow or returns an error, so duplicates can occur. Make processing idempotent by storing the Message-ID or a delivery identifier and skipping messages you have already handled.
How long should I keep inbound message content?
Only as long as you need it for the purpose, and no longer. Inbound mail often holds personal data, so set a retention period, restrict access, and delete raw messages and attachments once they have been processed and are no longer needed.