Sending email involves more than calling a PHP function. Your application creates a message, a configured transport accepts it, and mail infrastructure attempts delivery. A true return from mail() only means the message was accepted for delivery under that function's contract. It does not prove the message reached an inbox. This reference explains mail(), SMTP libraries, configuration differences, and safe local testing. It does not send any actual email. You will design a contact-form workflow with validated fields and a controlled sender, then learn how queues and delivery events make real applications easier to operate.
Understand the delivery chain
PHP builds the recipient, subject, body, and headers. A local mail transfer agent or an SMTP provider handles transmission according to the chosen transport. Receiving servers apply authentication, reputation, spam filtering, and recipient rules. A message can be accepted initially and later bounce, be delayed, or reach a spam folder. Treat application submission and confirmed delivery as different states. For account confirmation, keep a token pending until the user completes the confirmation link rather than assuming acceptance proves ownership. Store a message job identifier for troubleshooting. Do not promise immediate delivery in interface text unless your product can actually support that promise. A clear 'Check your email' message can also explain how to retry without creating many duplicate jobs.
Know what mail() does and does not provide
mail() uses transport configuration supplied by the PHP environment. On Unix-like systems it often invokes a configured sendmail-compatible program; Windows has different SMTP-related configuration behavior. Hosting providers may disable or restrict local mail. Inspect the applicable PHP configuration instead of assuming a copied mail example will work anywhere. mail() is not a general authenticated SMTP client with all modern transport features. For structured HTML, attachments, address formatting, authenticated SMTP, and TLS handling, use a maintained library or service API that fits your environment. Select the library based on documented capabilities and supported PHP versions, then pin dependencies in the lockfile. Do not hand-build multipart boundaries and encodings unless you have a concrete reason and strong tests.
Keep headers and sender identity controlled
Validate recipient addresses according to the feature's rules and do not place raw form fields into headers. Carriage returns or line feeds in header-derived input can lead to malformed or injected message headers. A contact form should normally send from an application-owned address and place a validated visitor address in Reply-To, rather than impersonating the visitor in From. This also fits domain authentication policies more naturally. Keep the subject bounded and sanitize it under the mail library's safe API contract. Never accept arbitrary command-line transport parameters from a visitor. Address validation confirms syntax, not mailbox ownership or permission to send. Enforce authorization and abuse limits so the form cannot become an open relay or spam tool.
Configure authentication and secrets deliberately
SMTP credentials belong in a secret store or protected runtime configuration, not source code, browser JavaScript, or public .env files. Require the transport security mode supported by the provider and verify certificates according to the library documentation. Domain-level SPF, DKIM, and DMARC configuration influences whether recipients trust the sender, but these settings must match your actual infrastructure. Do not invent DNS records from a generic tutorial. Use the provider's precise instructions and verify sending-domain ownership. Separate development and production credentials and recipient policies. A local mail-capture service can accept test messages without transmitting them externally. In automated tests, inject a fake transport and inspect constructed message objects rather than depending on a live external inbox.
Queue messages and handle outcomes
Important applications often enqueue mail work after the relevant database operation succeeds. Sending during a web request can make the page slow and leave awkward partial outcomes when the transport times out. A queue worker can retry transient failures with bounded backoff and mark permanent failures for review. Use a stable message or business-event key to prevent duplicates when jobs are retried. Do not retry every rejection forever; an invalid recipient differs from a temporary network outage. Log transport error codes and identifiers without exposing credentials or full confidential bodies. Delivery webhooks need their own authentication and replay policy. Update status carefully because an event may arrive more than once or out of order.
Design the content and local test workflow
Provide a plain-text version even when the message includes HTML, and escape dynamic text for HTML email templates. Avoid embedding private account data unnecessarily. Confirmation and reset links should use short-lived, single-purpose tokens whose secrets are not stored in plaintext where avoidable. Explain the action and expiration in ordinary language. In development, use a capture transport or fake sender that records the intended recipient, subject, and body. Check the message structure, headers, and escaped content without contacting real recipients. Before a production rollout, test the configured sender domain, bounce handling, rate limits, and unsubscribe requirements appropriate to the feature. Transport success should be logged as acceptance, with delivery tracked separately where available.
Practice task and worked solution
Design a password-reset mail job with a fake transport. The solution generates a short-lived one-time token, records the reset request under the account policy, and enqueues a message using an application-owned sender. The fake transport captures the message so tests can verify its recipient, controlled subject, plain-text content, and reset URL without external delivery. Give the job an idempotency key and classify transient versus permanent transport failures. Avoid exposing whether an email exists through public responses where the account threat model requires uniform messages. Review logs for secrets and confirm reset tokens do not appear in public error output.
Continue learning
Use form validation, security, and exceptions for message inputs and failures.
The PHP mail manual documents transport configuration and the meaning of its return value.
Email Sending and Mail Configuration Examples
Each example shows the code, the output it produces and what each part does.
Build a contact message without sending it
This CLI example validates a fixed visitor address and constructs a plain-text message description. It deliberately does not call mail() or contact SMTP.
Expected output is Website contact request, Reply-To: ada@example.com, and Transport: not invoked on separate lines. The example domains are placeholders, and no message is sent.
<?php
$visitor = 'ada@example.com';
$name = 'Ada';
$text = 'Please send the opening hours.';
if (filter_var($visitor, FILTER_VALIDATE_EMAIL) === false
|| str_contains($visitor, "\r") || str_contains($visitor, "\n")) {
throw new InvalidArgumentException('Invalid reply address');
}
$message = [
'from' => 'contact@example.org',
'to' => 'support@example.org',
'reply_to' => $visitor,
'subject' => 'Website contact request',
'body' => "Name: $name\nMessage: $text",
];
echo $message['subject'] . PHP_EOL;
echo 'Reply-To: ' . $message['reply_to'] . PHP_EOL;
echo 'Transport: not invoked' . PHP_EOL;Website contact request
Reply-To: ada@example.com
Transport: not invokedEmail Sending and Mail Configuration FAQ
Does mail() returning true mean inbox delivery?
No. It reports acceptance for delivery by the local mail system. The message can still be delayed, rejected by the receiving server, or filtered as spam. Use a proper mail service with delivery logs or webhooks when you need to know what happened.
Should From contain the visitor's email?
No. Sending from a visitor's address makes your server impersonate their domain, which modern checks such as SPF, DKIM and DMARC are designed to reject. Prefer an application-owned sender and a validated Reply-To containing the visitor's address, so replies still reach them.
Can SMTP secrets live in JavaScript?
No. Anything sent to the browser can be read by anyone who views the page source or network traffic, so a password there would let strangers send mail through your account. Keep SMTP credentials on the server, in environment variables or a secrets store.
How can tests avoid real mail?
Inject a fake or local capture transport. Code that depends on a mailer interface can receive a fake in tests that records messages for assertions, and in development a tool such as Mailpit catches all outgoing mail in a local inbox.
Email Sending and Mail Configuration Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A contact form puts raw visitor input in From. Choose the safer design.
Email Sending and Mail Configuration Quiz
2 questions to check what you learned.
What does a true mail() result establish?
- Acceptance for delivery by the configured mail system
- Guaranteed inbox placement
- Verified recipient ownership
- A completed password reset
Where should SMTP credentials be kept?
- Public JavaScript
- Protected server-side runtime configuration
- A committed example with real passwords
- The email subject