Back to Blog

How to Debug Email Headers and SMTP Responses

When an email fails to deliver, arrives in spam, or renders incorrectly, the answer is almost always in the headers and SMTP response codes. Knowing how to read them is a fundamental debugging skill for any developer working with transactional or notification emails.

This guide covers the structure of email headers, what SMTP response codes mean, how to trace an email's path through servers, and how to use command-line tools and SMTP sandboxes to diagnose common problems.

Email Header Fundamentals

Every email carries metadata in its headers. When you view an email in Gmail, Outlook, or any client, you are seeing the rendered body. The headers contain the operational data: who sent it, how it was routed, whether it passed authentication checks, and what content encoding was used.

Here is a simplified set of headers from a typical transactional email:

From: Acme App <[email protected]>
To: [email protected]
Subject: Your password has been reset
Date: Thu, 12 Mar 2026 14:22:08 +0000
Message-ID: <[email protected]>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="----=_Part_001"
Reply-To: [email protected]
List-Unsubscribe: <https://acme.com/unsubscribe?token=xyz>, <mailto:[email protected]>
X-Mailer: Laravel/12.x

Key Headers Explained

From -- The sender address displayed to the recipient. Must align with your domain's SPF and DKIM records. If From is [email protected] but the email is sent from a server not listed in acme.com's SPF record, receiving servers may reject or spam-fold the message.

To / CC / BCC -- Recipient addresses. BCC headers are stripped before delivery, so they should never appear in the received message. If they do, you have a bug.

Message-ID -- A globally unique identifier for the message. Format is typically <unique-string@sending-domain>. Email clients use this for threading. Duplicate Message-IDs across different emails will cause clients to group unrelated messages together.

MIME-Version -- Always 1.0. Its absence can cause parsing issues in some older clients.

Content-Type -- Defines the body format. Common values:

  • text/plain -- Plain text only
  • text/html -- HTML only
  • multipart/alternative -- Both text and HTML (recommended)
  • multipart/mixed -- Body with attachments

Reply-To -- Where replies should be directed. If omitted, replies go to the From address. Many applications use a no-reply@ From address with a specific Reply-To for routing responses to the right team.

List-Unsubscribe -- Required by Gmail and Yahoo for bulk senders. Must include either an HTTPS URL, a mailto: address, or both. Without this header, your emails are more likely to be flagged as spam.

SMTP Response Codes

SMTP is a text-based protocol. Every command you send to an SMTP server gets a three-digit response code followed by a human-readable message. Understanding these codes is essential for debugging delivery failures.

Success Codes (2xx)

Code Meaning
220 Server ready (greeting)
221 Server closing connection
235 Authentication successful
250 Requested action completed (OK)
251 User not local; will forward

Intermediate Codes (3xx)

Code Meaning
334 Server challenge (during AUTH)
354 Start mail input (after DATA command)

Transient Failure Codes (4xx)

These are temporary. The sending server should retry.

Code Meaning
421 Service not available, closing connection
450 Mailbox unavailable (busy or temporarily blocked)
451 Requested action aborted: local error in processing
452 Insufficient storage

Permanent Failure Codes (5xx)

These are final. The sending server should not retry.

Code Meaning
500 Syntax error, command not recognized
501 Syntax error in parameters
502 Command not implemented
503 Bad sequence of commands
530 Authentication required
535 Authentication credentials invalid
550 Mailbox unavailable (does not exist or policy rejection)
551 User not local
552 Message size exceeds limit
553 Mailbox name not allowed
554 Transaction failed

The most common codes you will encounter during debugging are 250 (success), 535 (bad credentials), 550 (recipient rejected), and 421 (server temporarily unavailable).

Enhanced Status Codes

Modern SMTP servers often include enhanced status codes in the format X.Y.Z:

550 5.1.1 <[email protected]>: Recipient address rejected: User unknown

The 5.1.1 breaks down as: 5 = permanent failure, 1 = addressing, 1 = bad destination mailbox. RFC 3463 defines the full list, but the pattern is consistent: the first digit mirrors the basic response code class.

Reading Received Headers to Trace Email Path

Every mail server that handles a message prepends a Received header. These headers form a chain from the originating server to the final destination. They are read bottom-to-top (the oldest is at the bottom).

Received: from mail-frontend.example.com (mail-frontend.example.com [198.51.100.10])
    by mx.recipient.com (Postfix) with ESMTPS id ABC123
    for <[email protected]>; Thu, 12 Mar 2026 14:22:10 +0000
Received: from app-server.acme.com (app-server.acme.com [203.0.113.5])
    by mail-frontend.example.com (Postfix) with ESMTP id DEF456
    for <[email protected]>; Thu, 12 Mar 2026 14:22:09 +0000

Reading bottom-to-top:

  1. app-server.acme.com (your application server) handed the message to mail-frontend.example.com (your outbound mail relay).
  2. mail-frontend.example.com delivered it to mx.recipient.com (the recipient's mail server).

Each Received header includes timestamps. If you see a large gap between consecutive timestamps, that server introduced a delay -- useful for diagnosing slow delivery.

Authentication Headers: SPF, DKIM, and DMARC

Modern email authentication relies on three mechanisms. When debugging deliverability issues, these headers tell you exactly what passed and what failed.

SPF (Sender Policy Framework)

SPF verifies that the sending server's IP address is authorized to send email for the domain in the MAIL FROM (envelope sender) address.

Received-SPF: pass (google.com: domain of [email protected]
    designates 203.0.113.5 as permitted sender)
Authentication-Results: mx.google.com;
    spf=pass (google.com: domain of [email protected]
    designates 203.0.113.5 as permitted sender) [email protected]

Common SPF failures happen when you switch email providers but forget to update your DNS TXT record, or when you send from a server IP that is not listed in your SPF record.

DKIM (DomainKeys Identified Mail)

DKIM attaches a cryptographic signature to the email headers. The receiving server verifies it against the public key published in DNS.

DKIM-Signature: v=1; a=rsa-sha256; d=acme.com; s=selector1;
    h=from:to:subject:date:message-id;
    bh=base64encodedHash;
    b=base64encodedSignature
Authentication-Results: mx.google.com;
    dkim=pass header.d=acme.com header.s=selector1

DKIM failures occur when the message body or signed headers are modified in transit (by a mailing list, a forwarding service, or a misconfigured relay).

DMARC (Domain-based Message Authentication, Reporting and Conformance)

DMARC ties SPF and DKIM together and tells receiving servers what to do when both fail.

Authentication-Results: mx.google.com;
    dmarc=pass (p=REJECT sp=REJECT) header.from=acme.com

A dmarc=fail result with a p=reject policy means the email was dropped entirely. With p=quarantine, it lands in spam. With p=none, it is delivered normally but the failure is reported to the domain owner.

Debugging with Telnet and OpenSSL

For hands-on SMTP debugging, nothing beats a raw connection to the server. This lets you see exactly what the server responds at each step.

Unencrypted Connection (Testing Against a Sandbox)

telnet sandbox.sendpit.com 1025
220 SendPit SMTP Ready
EHLO myhost.local
250-SendPit
250-AUTH PLAIN LOGIN
250 OK
AUTH LOGIN
334 VXNlcm5hbWU6
bWJfZGV2X21haWxib3g=
334 UGFzc3dvcmQ6
c2VjcmV0cGFzc3dvcmQ=
235 Authentication successful
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
250 OK
DATA
354 Start mail input; end with <CRLF>.<CRLF>
From: [email protected]
To: [email protected]
Subject: Test from telnet
Content-Type: text/plain

This is a test message sent via raw SMTP.
.
250 OK: message queued
QUIT
221 Bye

TLS-Encrypted Connection

For servers that require TLS (port 587 or 465):

openssl s_client -connect smtp.provider.com:587 -starttls smtp

This establishes a TLS connection and then drops you into an interactive SMTP session where you can issue the same commands as above.

What to Look For

  • Authentication failures (535): Wrong username/password, or the AUTH mechanism is not supported.
  • Recipient rejection (550): The server does not recognize the address, or policy blocks it.
  • Connection drops (421): Rate limiting, greylisting, or server overload.
  • Size limits (552): Your message exceeds the server's maximum. Common with large attachments.

Using an SMTP Sandbox to Inspect Headers

When you send email through an SMTP sandbox like SendPit, every message is captured with its full headers intact. This gives you a controlled environment to inspect exactly what your application is producing.

Typical workflow:

  1. Configure your application to send through the sandbox.
  2. Trigger the email (password reset, order confirmation, etc.).
  3. Open the sandbox web interface and find the captured message.
  4. Switch to the "Headers" or "Source" view to see raw headers.
  5. Verify From, Reply-To, Content-Type, List-Unsubscribe, and any custom headers.

This is significantly faster than sending through a production mail server and trying to find the message in a recipient's inbox. With a sandbox, the message is immediately available, and you can inspect it without any delivery pipeline interference.

// Send a test email and inspect it via the sandbox API
$response = Http::withToken(config('services.sendpit.api_key'))
    ->get('https://sendpit.com/api/v1/mailboxes/' . $mailboxId . '/messages');

$message = collect($response->json('data'))->first();

// Check headers programmatically
$headers = collect($message['headers']);
$contentType = $headers->firstWhere('name', 'Content-Type');
$replyTo = $headers->firstWhere('name', 'Reply-To');
$listUnsub = $headers->firstWhere('name', 'List-Unsubscribe');

expect($contentType['value'])->toContain('multipart/alternative');
expect($replyTo['value'])->toBe('[email protected]');
expect($listUnsub['value'])->toContain('https://acme.com/unsubscribe');

Common Header-Related Bugs

Character Encoding Issues

Subject lines and sender names with non-ASCII characters must be encoded per RFC 2047. If your mail library does not handle this, you will see raw encoded text:

Subject: =?UTF-8?Q?Ihre_Bestellung_wurde_best=C3=A4tigt?=

This is correct -- it is how the subject should look in the raw headers. If you see garbled characters in the rendered subject, the encoding declaration is wrong or missing. Most modern mail libraries handle this automatically, but custom header manipulation can break it.

Missing Multipart Boundary

If your Content-Type is multipart/alternative but the boundary parameter is missing or does not match the boundaries in the body, the email will render as raw MIME text:

Content-Type: multipart/alternative; boundary="----=_Part_001"

------=_Part_002    <-- WRONG: does not match the declared boundary
Content-Type: text/plain

Hello world

Duplicate or Missing Message-ID

Some mail libraries generate a Message-ID automatically. If you also set one manually, you may end up with duplicates. Some receiving servers reject messages with duplicate Message-ID headers. Conversely, missing Message-ID headers cause threading issues and may trigger spam filters.

Wrong Content-Type for Attachments

Setting Content-Type: application/octet-stream for every attachment technically works, but it prevents email clients from displaying inline previews. Use specific MIME types:

Content-Type: application/pdf; name="invoice.pdf"
Content-Type: image/png; name="logo.png"
Content-Type: text/csv; name="report.csv"

Reply-To Pointing Nowhere

A common oversight: setting Reply-To to an address that does not exist or is not monitored. Users hit "Reply," write a message, and it bounces. Always verify that Reply-To addresses have a working mailbox behind them.

Practical Debugging Walkthrough

Here is a real-world scenario: a user reports that password reset emails are not arriving.

Step 1: Check application logs. Look for SMTP errors in your application's log files. A 535 Authentication failed or Connection refused error tells you the problem is on the sending side.

# Laravel log
docker compose exec php grep -i "smtp\|mail\|swift" storage/logs/laravel.log | tail -20

Step 2: Verify SMTP connectivity. Can your application server reach the SMTP server at all?

docker compose exec php bash -c "echo 'QUIT' | nc -w 5 smtp.provider.com 587"

If the connection times out, it is a network issue -- firewall rules, security groups, or DNS resolution.

Step 3: Send a test email via raw SMTP. Bypass your application entirely to isolate whether the problem is in your code or the SMTP infrastructure.

openssl s_client -connect smtp.provider.com:587 -starttls smtp -quiet

Then walk through the SMTP conversation manually (EHLO, AUTH, MAIL FROM, RCPT TO, DATA, QUIT).

Step 4: Check the sandbox. If you are using an SMTP sandbox, check whether the email was captured there. If it appears in the sandbox but not in the real inbox, the issue is in your environment configuration -- you are sending to the sandbox instead of production, or the production SMTP credentials are wrong.

Step 5: Inspect headers of a delivered email. If the email was sent but landed in spam, look at the Authentication-Results header:

Authentication-Results: mx.google.com;
    spf=fail (google.com: 203.0.113.5 is not permitted)
    dkim=fail (signature verification failed)
    dmarc=fail (p=QUARANTINE)

This tells you exactly what failed. Fix your DNS records (SPF, DKIM) and the problem resolves.

Step 6: Verify the recipient address. A 550 5.1.1 User unknown response means the email address does not exist. Check for typos in the recipient address. If the address is dynamically generated (e.g., from user input), add validation:

// Laravel validation
$request->validate([
    'email' => ['required', 'email:rfc,dns'],
]);

The dns validation flag checks that the domain has MX records, catching obviously invalid domains before you attempt to send.

Summary

Email debugging is methodical. Start with SMTP response codes to understand whether the message was accepted. Read headers bottom-to-top to trace the delivery path. Check authentication headers to diagnose spam placement. Use raw SMTP connections to isolate infrastructure issues from application bugs. And use an SMTP sandbox to inspect your emails in a controlled environment before they ever touch a production mail server.

The tools are straightforward: telnet, openssl s_client, your application logs, and an SMTP sandbox like SendPit for capturing and inspecting messages. The hard part is not the tools -- it is knowing where to look. The headers will tell you everything if you know how to read them.

N

Nikhil Rao

Creator of SendPit. Building developer tools for email testing and SMTP infrastructure.

About SendPit →

More from the blog