Why Contact Form Emails Don't Arrive, and How to Fix It
· Updated: · Go Komura · Email Delivery, SPF, DKIM, DMARC, Inquiry Flow, B2B
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614629)
- First published
Cite this article(DOI: 10.5281/zenodo.21614628)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Why Contact Form Emails Don't Arrive, and How to Fix It. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614628 https://comcomponent.com/en/blog/2026/04/25/000-contact-form-email-delivery-troubleshooting/
- DOI (latest version)
- 10.5281/zenodo.21614628
- DOI (this version)
- 10.5281/zenodo.22220467
1. Executive Summary
When contact-form notification emails “send successfully but never arrive”, the cause is usually not the mail-sending code but the design of the sender. Three conclusions:
- Pin
From:to your own site’s domain, and put the form user’s address inReply-To:. The point is to line up the sender people see with the domain that actually gets authenticated. - Set up both SPF and DKIM. Forwarding breaks SPF easily, and relying on SPF alone drops legitimate mail along with the rest.
- Start DMARC at
p=nonefor observation, then move up toquarantine/rejectin stages. Jumping straight torejectstops your own legitimate mail too.
The reasoning and the evidence come in chapter 2 onward. If what you need right now is to isolate a delivery failure already in progress, it is fine to start reading at the diagnostic procedures in chapter 4.
Intended Readers and Assumed Environment
This article is written for anyone dealing with the problem that notification emails from their own site’s contact form are not arriving.
| Item | Details |
|---|---|
| Intended readers | The person responsible for the website, the developer who implemented the form, and the IT staff who look after mail inside the company |
| Parts that do not depend on the environment | Chapters 2 to 5 (how to think about the sender, failure scenarios, how to read headers, From design). The story is the same whatever your sending infrastructure is |
| Parts that assume an environment | The configuration examples in chapter 6. They are built around PHP and Linux-based sending infrastructure (Postfix / Exim / OpenDKIM), plus SendGrid, SES, and Mailgun |
| Not covered | The settings screens of individual CMSes and form plugins, internal mail flow in Exchange Online / Microsoft 365, and delivery design for email marketing |
The DNS records (SPF / DKIM / DMARC) and the header design apply whether the web server is Windows or Linux, and whether sending goes through your own MTA or an external service. If your implementation language differs, only two things need reinterpreting: where the headers get assembled, and where the envelope sender gets specified.
Terms Used in This Article
Here are the terms that appear in the body without further explanation.
| Term | Meaning |
|---|---|
| MTA (Mail Transfer Agent) | The server software that delivers mail. Postfix and Exim, but also sending services such as SendGrid and SES, play this role |
| MUA (Mail User Agent) | The mail software a person uses, such as Outlook or the Gmail interface |
| Envelope | The delivery-level sender and recipient exchanged during the SMTP conversation (MAIL FROM / RCPT TO). Separate from the From: and To: headers in the message body, in the way an envelope is separate from the letter inside it |
| Alignment | In a DMARC evaluation, the condition that the domain in the From: header and the domain authenticated by SPF or DKIM line up |
| Reputation | The receiver’s assessment of a sending IP or sending domain. As it degrades, mail gets marked as spam or refused |
| PTR | The reverse-lookup DNS record that resolves a host name from an IP address |
| DSN (Delivery Status Notification) | A notification of the delivery outcome. The machine-readable form of what people call a bounce message, defined by RFC 3464 |
| milter | The mechanism for inserting filter processing into an MTA. OpenDKIM, which applies DKIM signatures, runs through this mechanism |
How This Article Is Organized
- Executive summary (this chapter)
- The roles of SPF, DKIM, DMARC, and the From header - understanding the mechanism
- Failure scenarios common to contact forms
- Diagnostic procedures and commands - headers, DNS, SMTP, real sends
- Recommended From design patterns
- Configuration guide by setup (external SMTP / shared hosting / PHP)
- Troubleshooting checklist
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (22 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. The Roles of SPF, DKIM, DMARC, and the From Header
In mail delivery you have to keep the envelope and the headers apart. What SPF mainly validates is the MAIL FROM given during the SMTP session, which is the delivery-level sender. At final delivery exactly one copy of that return path should be recorded as Return-Path, and a sending SMTP system should not construct messages that already carry a Return-Path header. From:, by contrast, lives on the message-header side and expresses who the mail appears to be from; Reply-To: names the reply target, and Sender: names the agent that actually transmitted the message.
RFC 5322 defines From: as the author of the message and Sender: as the actual transmitting agent. If the author and the transmitting agent are the same, Sender: should not be used, and when you want the reply target to be someone other than the author, Reply-To: is the correct tool. RFC 5322 further states explicitly that From: should not carry an address that does not belong to the author. A contact-form notification is normally not sent by the user from their own MUA - it is a notification the site’s own system constructs - so putting the user’s address in From: also drifts away from what the specification means.
DKIM attaches a signature over a subset of the headers and the body, and the receiver verifies it against a public key published in DNS. The signing domain appears as d= in the DKIM-Signature, and the public key is looked up through a selector, as in selector._domainkey.example.com. DKIM gets used as the authentication that survives forwarding comparatively well, but if the body or a signed header is modified in transit, the bh= body hash or the signature verification fails.
DMARC looks at more than whether SPF and DKIM passed: it looks at whether the domains they authenticated are consistent with the From: domain. That is the crux. Even when SPF passes, MAIL FROM=bounces.vendor.net combined with From: contact@example.com means SPF alignment fails. If DKIM passes with d=example.com, DMARC can still pass - but with no DKIM either, DMARC fails. DMARC defines strict / relaxed alignment modes through adkim / aspf, the policies p=none|quarantine|reject, and the report destinations rua / ruf.
Gmail’s recent guidelines take this design philosophy and turn it directly into operational requirements. Google states explicitly that you must not spoof the From: header, and that for direct mail the domain inside the From: header must match either the SPF domain or the DKIM domain. A contact-form notification is not marketing mail, but the basic logic of receiver-side filters is the same, so ignoring this thinking costs you delivery rate.
sequenceDiagram
participant User as Form user
participant App as Web app
participant SMTP as Sending MTA / SMTP service
participant DNS as DNS
participant MX as Receiving MX
User->>App: Submit the form
App->>App: Decide From / Reply-To / Sender
App->>SMTP: SMTP send request
SMTP->>SMTP: Apply DKIM signature
SMTP->>MX: MAIL FROM / RCPT TO / DATA
MX->>DNS: SPF lookup (MAIL FROM)
MX->>DNS: DKIM public key lookup (selector._domainkey)
MX->>DNS: DMARC lookup (_dmarc + From domain)
MX->>MX: Alignment evaluation
MX-->>App: Delivered / spam / rejected / bounced
What matters in this flow is that DMARC’s evaluation stays centered on the From: domain right to the end. The app touches From:, the SMTP side touches MAIL FROM, and the DNS side holds SPF/DKIM/DMARC - three separate places - so fixing only one of them does not clear up the non-delivery.
3. Failure Scenarios Common to Contact Forms
The most common by far is putting the form user’s address into From:. If you send through the site’s SMTP but set From: taro@gmail.com, what passes SPF or DKIM is normally the site’s domain, not Gmail’s. The result is a gap - From: says Gmail while the authenticated domain is example.com - and DMARC alignment fails. Google itself asks you to avoid From: spoofing and to match From: with the SPF/DKIM domains.
Next most common is SPF breaking on forwarding. With mail forwarding, the source IP as the final recipient sees it tends to be a relay server that is not listed in the original sending domain’s SPF, so even legitimate mail fails SPF. Google likewise advises that forwarded mail fails SPF easily, so you should always use DKIM. On top of that, if the relay rewrites the body, adds a subject prefix, or appends a footer, DKIM breaks as well.
Incomplete initial setup on an external SMTP service is another classic. With SendGrid there are cases where you cannot send until Domain Authentication is configured; turning Automated Security ON generates CNAME-based authentication records, and you can set a Custom Return Path or a Custom DKIM Selector as needed. With SES the default MAIL FROM uses amazonses.com, so SPF holds implicitly - but if you want SPF alignment with your site’s domain, you have to configure a custom MAIL FROM. With Mailgun too, until the sending domain’s SPF/DKIM and the required MX records are in place, a correct sending signature never materializes.
The local MTA on shared hosting is an easily overlooked landmine. Even when your site is authenticated, delivery suffers if the IP the mail actually leaves from is a shared IP with a poor reputation, has no PTR, or if the hosting provider does not apply DKIM. Google makes a PTR on the sending IP a requirement, and notes that a poor shared-IP reputation can also cause 5.7.1-class errors.
How PHP’s mail() or a sending library gets used is another easy place to go wrong. The PHP manual explains that mail() needs a From header, and that the additional-parameters argument lets you specify the envelope sender through sendmail -f. In other words, the sound approach is handing the envelope sender to the MTA, not assembling a Return-Path: header yourself. Misread this and the sender used for the SPF evaluation diverges from the sender the application assumes.
Finally, there is the case of reaching for Sender or From where Reply-To is what you want. If all you need is for replies to go to the user, Reply-To is enough. Sender is the header for stating explicitly that the author and the actual transmitting agent differ - not something to use routinely on a contact form. Separating the design goal - do you want replies to go back to the user, or do you want to indicate who is responsible for the notification - is what keeps things from breaking.
4. Diagnostic Procedures and Commands
The first thing to do is look at the raw mail headers before you look at the code. In Gmail that is Show original; in Outlook it is Message details or Internet headers. From there you can check Authentication-Results, Return-Path, From, Reply-To, DKIM-Signature, and Received. Those headers let you tell, with considerable precision, whether the problem is that the mail never went out or that authentication and alignment have broken down.
What to Look at First in the Headers
These five come first.
- What domain is in
From: - What domain is in
Return-Path: - Does
Authentication-Results:showspf=pass/dkim=pass/dmarc=pass - When
dkim=pass, which domain is inheader.i=ord= - When
dmarc=fail, is the reason an authentication failure or an alignment failure
Google’s DMARC troubleshooting guidance also states plainly that a message can pass the other authentications and still fail DMARC when the headers do not align.
Commands for Checking DNS
dig is the standard tool for DNS troubleshooting; the BIND manual describes it as a DNS lookup tool with flexible, clear output. nslookup is a lighter check that also works in non-interactive mode. For contact-form authentication checks, query at least the three records: SPF, DKIM, and DMARC.
# SPF
dig +short TXT example.com
# DKIM
dig +short TXT form2026._domainkey.example.com
# DMARC
dig +short TXT _dmarc.example.com
# On Windows
nslookup -type=txt example.com
nslookup -type=txt _dmarc.example.com
nslookup -type=txt form2026._domainkey.example.com
Expected output looks like this.
"v=spf1 include:sendgrid.net include:mailgun.org ip4:203.0.113.10 -all"
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
"v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
What you are looking for here is whether the SPF record is consolidated into one, whether the DKIM public key resolves, and whether DMARC has a p=. RFC 7208 sets a limit of ten mechanisms that trigger DNS lookups in an SPF evaluation, and exceeding it causes permerror.
Checking the SMTP Connection and TLS
openssl s_client is OpenSSL’s general-purpose SSL/TLS client, and it is handy for checking an SMTP server’s STARTTLS and certificate chain. -starttls smtp starts SMTP STARTTLS, and -showcerts shows the list of certificates the server returned.
printf 'QUIT\r\n' | openssl s_client \
-connect smtp.example.com:587 \
-starttls smtp \
-servername smtp.example.com \
-showcerts \
-brief
Example output.
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Verification: OK
250 CHUNKING
At minimum this tells you whether the SMTP server is reachable, whether STARTTLS is enabled, and whether certificate verification shows any obvious problem. Google requires TLS of senders above a certain volume, and not using TLS can cause 5.7.29.
Reproduction Test with a Real Send
swaks is a practical tool built specifically for SMTP testing, and it reproduces sends flexibly, including TLS, authentication, and SMTP extensions. When investigating contact-form non-delivery, it works well to send exactly one message that bypasses the app, using the same SMTP, the same From, the same Reply-To, and the same recipient.
swaks \
--server smtp.example.com \
--port 587 \
--tls \
--auth LOGIN \
--auth-user contact@example.com \
--auth-password '********' \
--from bounce@example.com \
--to yourtest@gmail.com \
--h-From "Site Notifications <contact@example.com>" \
--h-Reply-To "Taro Yamada <visitor@gmail.com>" \
--header "Subject: swaks test" \
--body "This is a test"
A typical successful send looks like this.
=== Trying smtp.example.com:587...
=== Connected to smtp.example.com.
<- 250-STARTTLS
<- 250-AUTH LOGIN PLAIN
-> STARTTLS
<- 220 Ready to start TLS
...
<- 250 2.0.0 Ok: queued as ABC123DEF
If it fails through the app but goes through with swaks, the cause most likely sits in the library’s header assembly or the envelope-sender configuration. If swaks fails the same way, you can narrow the problem down to DNS, SMTP, or receiver-side policy.
External Diagnosis with mail-tester
mail-tester issues a random test address, has you send mail to it, then analyzes the message, the sending server, and the sending IP, and returns a detailed report. It suits a first-pass diagnosis for the cases on local MTAs and shared servers where mail just somehow does not arrive.
Using it is simple.
- Get the test address mail-tester issues
- Send one message through the same path the contact form uses
- Review the score and the findings on SPF / DKIM / DMARC / reverse DNS / blocklists / body composition
Do not judge production delivery on the mail-tester score alone, but at minimum it surfaces things like SPF not being visible at all, a DKIM public key that will not resolve, or a body or sender composition that looks off.
Header Analysis Samples
A failing example.
Return-Path: <bounce-123@vendor.example.net>
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of bounce-123@vendor.example.net designates 198.51.100.10 as permitted sender) smtp.mailfrom=vendor.example.net;
dkim=none;
dmarc=fail (p=quarantine sp=quarantine dis=none) header.from=gmail.com
From: Taro Yamada <visitor@gmail.com>
Reply-To: Taro Yamada <visitor@gmail.com>
Subject: Inquiry
This message passes SPF itself, yet fails DMARC because From: is gmail.com. It is the classic way a contact form breaks when the user’s address goes into From:.
A passing example looks like this.
Return-Path: <bounce@example.com>
Authentication-Results: mx.google.com;
spf=pass smtp.mailfrom=example.com;
dkim=pass header.i=@example.com header.s=form2026;
dmarc=pass header.from=example.com
From: Example Site <contact@example.com>
Reply-To: Taro Yamada <visitor@gmail.com>
Subject: Inquiry notification
In this shape, Reply-To: keeps it easy to reply to the user, while the visible sender and the authenticated sender both land on example.com - and deliverability becomes far more stable.
5. Recommended From Design Patterns
The principle that does not waver on a contact form is to line up the domain used for authentication with the From domain shown to recipients. On top of that, move only the reply target out into Reply-To:. Splitting the design into three layers - Sender: only when it is needed, and Return-Path set on the envelope - keeps it stable.
| Pattern | Header example | Suited to | Strengths | Watch out for |
|---|---|---|---|---|
| Recommended pattern | From: contact@example.comReply-To: visitor@gmail.comReturn-Path: bounce@example.com |
Nearly every contact form | Easy to pass DMARC / easy to reply to / simple to implement | Forget Reply-To and replies go back to the site |
| Subdomain separation | From: contact@form.example.comReply-To: visitor@gmail.comReturn-Path: bounce.form.example.com |
Separating form notifications from your primary mail | Easy reputation separation / easy to manage | SPF/DKIM/DMARC have to be set up on the subdomain as well |
Explicit Sender |
From: contact@example.comSender: mailer@example.comReply-To: visitor@gmail.com |
Special requirements to declare the sending agent | Shows who is operationally responsible | Normally unnecessary. Redundant when author and sender are the same |
| Anti-pattern | From: visitor@gmail.comReply-To: visitor@gmail.com |
Implementations that only want the reply target to stand out | Looks natural, and nothing more | A frequent cause of DMARC failure. Avoid it for inquiry notifications |
The grounds for this table are RFC 5322’s semantics for From / Sender / Reply-To, RFC 5321’s handling of Return-Path, and the specification that DMARC judges alignment against From:. For form notifications, the recommended pattern in the first row is enough as the default. Even when you feel the pull to put the user’s address in From:, putting the reply target in Reply-To: achieves the goal.
The one thing worth committing to memory: Return-Path is not a header you edit but the result of the envelope sender used during delivery. With PHP mail() that means -f; with an SMTP service it means configuration items such as Custom MAIL FROM, Return Path, or bounce domain. That is the correct implementation.
6. Configuration Guide by Setup
From here we work through the thinking behind the configuration for the three setups seen most often in the field. As a premise, prefer the exact values each service’s admin console produces for what you put into DNS. The record examples below are representative ones, meant to make the structure clear.
When the Site Uses an External SMTP Service
The single most important point with external SMTP is to finish authenticating your own domain first. Google likewise advises that if you use an email service provider, you confirm the service authenticates your domain’s SPF and DKIM.
Recommended configuration
From:iscontact@example.comorcontact@form.example.comReply-To:is the form user’s addressReturn-Path/ MAIL FROM is a bounce subdomain you control, such asbounce.example.com- DKIM signs with
example.comor the sending subdomain - DMARC sits on the visible
From:domain
Typical SendGrid setup
SendGrid presumes Domain Authentication, and with Automated Security ON it generates three CNAMEs. With it OFF it generates one MX and two TXT records, and you can also configure a Custom Return Path and a Custom DKIM Selector.
; Example: SendGrid (use the values generated in the admin console)
em123.example.com. CNAME u123456.wl.sendgrid.net.
s1._domainkey.example.com. CNAME s1.domainkey.u123456.wl.sendgrid.net.
s2._domainkey.example.com. CNAME s2.domainkey.u123456.wl.sendgrid.net.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
The state people get stuck in with SendGrid is having finished SMTP authentication while domain authentication was never configured. Sending itself still works in that state, but from the receiver’s point of view the relationship between From: and the authenticated domain is weak. The baseline is to get Domain Authentication through, then configure a Custom Return Path if you need one.
Typical SES setup
SES uses a MAIL FROM on an amazonses.com subdomain by default, so SPF holds implicitly. If you want SPF alignment with your site’s domain, though, use a custom MAIL FROM. SES then requires an SPF TXT and an MX on the custom MAIL FROM domain, and there must be exactly one MX. Easy DKIM additionally adds three CNAMEs to DNS.
; Example: SES Easy DKIM
abcde12345._domainkey.example.com. CNAME abcde12345.dkim.amazonses.com.
fghij67890._domainkey.example.com. CNAME fghij67890.dkim.amazonses.com.
klmno54321._domainkey.example.com. CNAME klmno54321.dkim.amazonses.com.
; Example: SES custom MAIL FROM
bounce.example.com. MX 10 feedback-smtp.ap-northeast-1.amazonses.com.
bounce.example.com. TXT "v=spf1 include:amazonses.com -all"
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
What matters in the SES design is that the MAIL FROM domain should be a bounce-dedicated subdomain, not the sending From: domain itself. AWS also advises making MAIL FROM a subdomain rather than the very domain you send mail from.
Typical Mailgun setup
When you verify a sending domain with Mailgun you need a TXT for SPF and a TXT for DKIM, plus two MX records. If SPF already exists, do not add another SPF record - splice include:mailgun.org into the existing one. You may see several DKIM keys, but sending works as long as the key currently in use is published correctly in DNS.
; Example: Mailgun on a subdomain
mg.example.com. TXT "v=spf1 include:mailgun.org ~all"
mx._domainkey.mg.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
mg.example.com. MX 10 mxa.mailgun.org.
mg.example.com. MX 10 mxb.mailgun.org.
email.mg.example.com. CNAME mailgun.org.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
Mailgun works well with subdomain operation, so carving out a dedicated sending subdomain such as mg.example.com makes form notifications and transactional notifications easier to manage.
When the Site Sends via the Local MTA on Shared Hosting
On shared hosting, the first thing to examine is the quality of the hosting provider’s sending infrastructure, not your own application. If the sending IP’s PTR, DKIM support, the shared IP’s reputation, or visibility into sending logs is weak, the setup is at a disadvantage on that alone. Google places weight on the sending IP’s PTR, and notes that a poor shared-IP reputation can be a cause of blocking.
In practice, this order is the safe one.
- Confirm whether the hosting company lets you configure SPF/DKIM/PTR through the control panel or via support
- Always pin
From:to your own domain - Include the hosting provider’s sending IPs, or its permitted sending domain, in SPF
- Enable DKIM through the hosting feature. If there is none, switch to external SMTP
- If you can, split the MAIL FROM bounce address out as something like
bounce.example.com
On a shared hosting plan, start by asking your provider
On a shared hosting plan, how much of the sending infrastructure you can touch yourself is limited. Before you start changing anything, check the following through the control panel, the manual, or the support desk.
| What to check | What it means if you cannot check it, or the answer is no |
|---|---|
| Whether outgoing mail can be DKIM-signed with your own domain | Without DKIM, the moment forwarding breaks SPF, DMARC goes down with it |
| Whether you can edit the SPF record yourself (including when DNS is managed by another company) | You cannot add your sending source to SPF, so you cannot fix spf=fail |
| Which IP is used for sending, and what its PTR (reverse DNS) looks like | A missing reverse lookup becomes a reason for the receiver to refuse the mail |
| Whether the IP is shared or dedicated | On a shared IP you are exposed to the sending quality of the other tenants |
| Whether you can view mail delivery logs | You cannot tell whether the message went out or was refused |
Whether you can specify the envelope sender (the equivalent of -f) |
You cannot control the bounce destination or the sender used in the SPF evaluation |
Of these, whether DKIM signing is available and whether delivery logs are available decide whether you can investigate and improve anything in this environment. If both answers are no, moving just the form notifications onto external SMTP is the quick route. Form notifications carry low volume and the switch has a limited blast radius, which makes them one of the easier things to migrate first.
As a typical example of generating DKIM yourself on shared hosting, in the OpenDKIM family opendkim-genkey will generate both the private key and the TXT record for DNS. Publishing the DKIM public key at a selector-qualified selector._domainkey.example.com matches the structure RFC 6376 describes.
mkdir -p /etc/opendkim/keys/example.com
opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s form2026
chown opendkim:opendkim /etc/opendkim/keys/example.com/form2026.private
chmod 600 /etc/opendkim/keys/example.com/form2026.private
Here is what it looks like after generation.
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
# /etc/opendkim/KeyTable
form2026._domainkey.example.com example.com:form2026:/etc/opendkim/keys/example.com/form2026.private
# /etc/opendkim/SigningTable
*@example.com form2026._domainkey.example.com
That said, if you cannot control the PTR or the outbound relay yourself on shared hosting, moving to external SMTP is the shorter path. Even at the low volumes of contact-form notifications, a weakly authenticated local MTA is at a disadvantage against Gmail and corporate mail.
When the Site Uses PHP mail() or an SMTP Library
PHP mail() is convenient, but from the standpoint of authentication and deliverability it depends on what the MTA behind it actually is. The PHP manual explains that mail requires a From header, and that when sending through sendmail_path you can specify the envelope sender in the additional parameters. Put the other way round, using mail() does not put SPF/DKIM/DMARC in order for you.
Design for this minimum first.
From:iscontact@example.comReply-To:is the form user- The envelope sender is
bounce@example.com - State the user’s address in the body as well
- If you can, use authenticated SMTP instead of
mail()
Minimal mail() example
Dropping user input straight into headers is dangerous. If CR/LF gets into $name or $email, an attacker can insert extra headers such as Bcc:, turning the form into a spam relay. The PHP manual likewise instructs that external input used in headers must always be validated and normalized. In the example below, every value that gets mixed into a header is sanitized in advance, including the envelope-sender argument (additional_params).
<?php
// Return only values that are safe to use in a header. Reject anything containing CR/LF/NUL.
function sanitize_header_value(string $value): string {
if (preg_match('/[\r\n\0]/', $value)) {
throw new InvalidArgumentException('Invalid characters in header value');
}
return trim($value);
}
// Validate the email address against the RFC.
function sanitize_email(string $email): string {
$clean = sanitize_header_value($email);
if (!filter_var($clean, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
return $clean;
}
$to = 'ops@example.com';
$subject = sanitize_header_value('Inquiry notification');
// $name / $email / $message are form input. $message goes into the body, so CR/LF is allowed there,
// while $name / $email are used in headers, so CR/LF must always be rejected.
$safeName = sanitize_header_value($name);
$safeEmail = sanitize_email($email);
$body = <<<TEXT
Name: {$safeName}
Email: {$safeEmail}
{$message}
TEXT;
$headers = [
'From' => 'Example Site <contact@example.com>',
'Reply-To' => sprintf('%s <%s>', $safeName, $safeEmail),
'Content-Type' => 'text/plain; charset=UTF-8',
];
// additional_params also reaches the shell, so use fixed values only and never mix in dynamic input.
mail($to, $subject, $body, $headers, '-fbounce@example.com');
There are two points in this example. One is that Return-Path: is not written as a header - the envelope sender is passed through -f in the fifth argument. The other is that user input mixed into headers such as Reply-To: goes through a sanitizer that rejects CR/LF. Assemble headers without sanitizing and an attacker can pour in a string like \r\nBcc: victim@example.com to insert extra headers, which is why the PHP manual treats validating external input used in headers as mandatory. Since additional_params ultimately reaches the shell, pass fixed values and keep user input out of it.
Example using an SMTP library
<?php
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->Port = 587;
$mail->SMTPAuth = true;
$mail->SMTPSecure = 'tls';
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->setFrom('contact@example.com', 'Example Site');
$mail->addAddress('ops@example.com');
$mail->addReplyTo($email, $name);
// Some libraries let you set Sender / return-path separately
$mail->Sender = 'bounce@example.com';
$mail->Subject = 'Inquiry notification';
$mail->Body = $body;
$mail->send();
The advantage of an SMTP library is that it makes the header sender and the envelope sender easy to control separately. For contact-form use it fits best with the design that pins From: to the site’s domain and puts only the reply target in Reply-To:.
Concrete SPF, DKIM, and DMARC Examples
Basic SPF example
example.com. TXT "v=spf1 ip4:203.0.113.10 include:sendgrid.net -all"
SPF has to include every source you actually send from. When you use a third-party sender, Google likewise asks you to confirm that sender is authenticated by your SPF and DKIM. Note also that SPF limits the number of DNS lookups, so be careful about stacking up includes.
Basic DKIM example
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
Selectors exist for key rotation, and they let several public keys coexist on the same domain. In practice, a name you can tell apart by purpose or by date reads better later than default.
DMARC rollout examples
Start with observation mode.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
Next, when you want to quarantine a portion.
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-agg@example.com; adkim=r; aspf=r"
Finally, strict operation.
_dmarc.example.com. TXT "v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-agg@example.com; adkim=s; aspf=s"
p=none means monitor only, quarantine recommends quarantining, and reject recommends refusal during the SMTP transaction. adkim and aspf switch between strict and relaxed. Rather than going straight to reject, it is safer to observe your volume and your legitimate sending sources at none and then raise the policy in stages.
If you also send rua / ruf to an outside aggregation service, RFC 7489 requires an additional DNS record on the third party’s side. To send example.com’s reports to thirdparty.example.net, for instance, the receiving side has to publish example.com._report._dmarc.thirdparty.example.net TXT "v=DMARC1".
7. Troubleshooting Checklist
Finally, here is a checking order you can use in the field as it stands. With contact-form non-delivery, working down the list from the top is the quick route.
Check First
- Is
From:on your own domain - Is the user’s address in
Reply-To: - Does
Authentication-Resultsshowspf=passordkim=pass, and on top of thatdmarc=pass - If
dmarc=fail, is it an authentication failure or an alignment failure - Is SPF consolidated into a single record
- Is the SPF lookup count not too high
- Does the DKIM public key resolve
- Does DMARC have a
p= - Are the sending IP’s PTR and reverse lookup reasonable
- Are you on a shared IP, and if so has its reputation degraded
Where to Look in Bounces and Logs
If bounce messages are coming back, the important fields inside the message/delivery-status DSN are Final-Recipient, Status, Action, and Diagnostic-Code. RFC 3464 defines this machine-readable delivery-failure information. A line such as Diagnostic-Code: smtp; 550 relay not permitted means the refusal came from the SMTP side, not the application layer.
On the server side, look at least at the MTA’s delivery logs. Postfix will show delivery successes and failures, queue backlog, relay refusals, DNS resolution failures, and DKIM milter warnings. Exim is the same. Here are the commands people reach for most.
# Example: Postfix
journalctl -u postfix -n 200 --no-pager
postqueue -p
# Example: Exim
exim -bp
Log paths and command permissions differ by hosting provider, so confirm up front whether you can see the mail delivery logs, not the application logs. In an environment where those are invisible, consolidating onto external SMTP makes the problem much easier to analyze.
How to Look in Gmail and Outlook
In Gmail you get the raw headers from Show original; in Microsoft Outlook, from Message details or Internet headers. When investigating contact-form non-delivery, saving and comparing the full header text beats screenshots.
Reading Receiving-Side Errors
Gmail-family errors make the cause easy to read off the code.
| Example error | Meaning | Main remedy |
|---|---|---|
5.7.27 |
SPF failure | Add the sending source to the SPF record |
5.7.30 |
DKIM failure | Fix the DKIM key and signing configuration |
4.7.32 |
From: does not match the SPF/DKIM organizational domain |
Revisit the From: design |
5.7.25 |
PTR / reverse DNS deficiency | Set up reverse DNS for the sending IP |
Google’s FAQ spells out these errors and how to deal with them. The one that shows up most with contact forms is 4.7.32, the alignment mismatch.
The Final Yardstick
If the following three conditions hold at the same time, the design of your contact-form notifications is solid.
From:is underexample.comReply-To:is the form user’s addressAuthentication-Resultsshowsdmarc=pass
With those three in place, the design is sound whether you use SendGrid, SES, Mailgun, shared hosting, or an SMTP library. If even one of them is missing, suspecting the From: design first is the shortest route.
Reference Links
Primary sources for the specifications and for each service. For the actual values you put into DNS, always prefer the values the service you use presents.
Specifications (RFCs)
- RFC 5321 - Simple Mail Transfer Protocol - the envelope,
MAIL FROM, and the handling ofReturn-Path - RFC 5322 - Internet Message Format - the semantics of
From:,Sender:, andReply-To: - RFC 7208 - Sender Policy Framework (SPF) - the SPF specification, including the DNS lookup limit
- RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures - selectors,
d=, and what gets signed - RFC 7489 - Domain-based Message Authentication, Reporting, and Conformance (DMARC) - alignment,
p=, and external report destinations - RFC 3464 - An Extensible Message Format for Delivery Status Notifications - how to read a bounce (DSN)
Receiver-Side Guidelines
- Email sender guidelines - Gmail Help - SPF / DKIM / DMARC, PTR, and the
From:matching requirement - Gmail SMTP errors and codes - Google Workspace Admin Help - what the
5.7.x/4.7.xcodes mean and how to handle them
Sending Services and Implementation
- How to Set Up Domain Authentication - SendGrid - the CNAME method and Custom Return Path
- Configuring a custom MAIL FROM domain - Amazon SES - the MX and SPF requirements
- Easy DKIM in Amazon SES - the three CNAMEs
- Domains - Mailgun Documentation - the records needed to verify a sending domain
- PHP: mail - Manual - header handling and
additional_params(-f) - swaks - Swiss Army Knife for SMTP - real send testing
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Designing Bulk Email for Small Businesses Without Locking Into a Specific Service
We lay out how a small business can avoid Bcc blast sending and design announcement email at the scale of dozens to hundreds of recipient...
Why Your Company Should Have a Website - Going Beyond a Brochure and Driving Profit
We lay out why a company should have a website and how it leads to profit within the flow from search to comparison, inquiry, and winning...
How to Build Service Pages - An Organizing Procedure for Technical B2B
For technical B2B sites, we lay out how to organize the role, headings, copy, CTAs, and inquiry flow of a service page.
Information Security 10 Major Threats 2026 — How to Read the Ranking, and What SMEs Should Actually Guard Against
In IPA's 'Information Security 10 Major Threats 2026,' ransomware attacks took first place for the 11th year running, supply chain attack...
Don't Forget to Decide 'How Many Seconds Is Fast Enough' — Organizing Non-Functional Requirements With IPA's Non-Functional Requirements Grade
Disputes like 'it's too slow' or 'we didn't expect that failure response' usually trace back to non-functional requirements nobody decide...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Web Development & SEO Topics
Topic hub for website development, SEO, inquiry flow, and internal-link design.
Where This Topic Connects
This article connects naturally to the following service pages.
Website Development
Contact form design, sender-address operations for notification emails, and Reply-To design — sorting out the whole inquiry flow — is a topic that pairs naturally with website development.
Technical Consulting & Design Review
Choosing among SPF / DKIM / DMARC, external SMTP services (SendGrid / SES / Mailgun), shared hosting, and PHP mail() is easy to handle as a design review tailored to your current configuration and requirements.
Frequently Asked Questions
Common questions about the topic of this article.
- What is the most common reason contact form emails fail to arrive?
- It is not the SMTP connection itself, but a mismatch between the visible sender (From:) and the sender that actually gets authenticated (SPF's MAIL FROM, or DKIM's d=). The most frequent form of this is a design that drops the form user's Gmail address straight into From:. If the mail goes out through the site's own SMTP but carries From: taro@gmail.com, the domain that gets authenticated is the site's while From: says Gmail, and that gap makes DMARC alignment fail.
- How should I set the From header on contact form notification emails?
- The baseline is to pin From: to your own site's domain and put the form user's address in Reply-To:. That keeps replies easy while the visible sender and the authenticated sender line up on the same domain, which is what stabilizes deliverability. Use Sender: only when the author and the actual transmitting agent genuinely differ, and set Return-Path as the envelope sender at the MTA or mail service rather than hand-writing the header.
- What should I do first when investigating undelivered mail?
- Look at the raw headers of a message that did arrive, before you look at any code. In Gmail, open Show original and check Authentication-Results, Return-Path, From, and DKIM-Signature. The top priorities are the domain in From:, the domain in Return-Path:, and whether spf, dkim, and dmarc all pass. When dmarc=fail, work out whether it is an authentication failure or an alignment failure. On the DNS side, query the three SPF, DKIM, and DMARC records with dig or nslookup.
- Can I set DMARC straight to reject?
- Rather than going straight to reject, it is safer to start at p=none in observation mode, confirm your volume and your legitimate sending sources, and then move up through quarantine to reject in stages. p=none means monitor only, quarantine recommends quarantining, and reject recommends refusal during the SMTP transaction. Forwarding also breaks SPF easily, and DKIM breaks too when a relay rewrites the body or headers, so it matters that you enable DKIM instead of depending on SPF alone.