Sending Email

JA JASWANT Updated 17 Oct 2026
7 min read ·Lesson 106 of 109
In this post

Web applications send email constantly: account verification, password resets, order confirmations, invoices, and contact-form notifications. Sending a message that actually arrives — in the inbox, no

Web applications send email constantly: account verification, password resets, order confirmations, invoices, and contact-form notifications. Sending a message that actually arrives — in the inbox, not the spam folder — requires more than calling one function. This lesson covers PHP's built-in mail() function and its limits, header injection, sending with a mail library over SMTP, building good message content, and deliverability.


PHP's mail() Function

Syntax

mail(string $to, string $subject, string $message, array|string $additional_headers = [], string $additional_params = ""): bool
<?php
$sent = mail(
    'asha@example.com',
    'Welcome to GoCourse',
    "Hi Asha,\n\nThanks for signing up!\n",
    [
        'From' => 'GoCourse <no-reply@gocourse.in>',
        'Reply-To' => 'support@gocourse.in',
        'Content-Type' => 'text/plain; charset=UTF-8',
    ]
);

var_dump($sent);

How It Works

  • mail() hands the message to the server's local mail transfer agent (the sendmail program on Linux, configured by sendmail_path), which then delivers it.
  • Headers can be passed as an associative array (PHP 7.2+).
  • The return value true only means that the local mail system accepted the message for delivery — not that it reached the recipient.

Limitations of mail()

LimitationConsequence
Needs a configured mail server on the machineOften unavailable on Windows, in containers, and on many hosts
No SMTP authentication or TLS settingsCannot send through Gmail, Amazon SES, or other providers directly
No helpers for HTML + text, attachments, or encodingComplex MIME formatting must be built by hand
Messages from web servers often lack SPF/DKIM alignmentHigh chance of landing in spam
No useful error informationFailures are hard to diagnose

For these reasons, real applications use a mail library that sends through an SMTP server or an email provider's API.


Header Injection

If user input is placed into email headers, a newline in that input can add new headers:

// DANGEROUS: $email comes from a contact form
mail('support@gocourse.in', 'Contact form', $message, "Reply-To: $email");

An attacker submits the "email" x@example.com\r\nBcc: victim1@example.com, victim2@example.com, turning your contact form into a spam relay. Header values must never contain line breaks:

<?php
function isSafeHeaderValue(string $value): bool
{
    return !preg_match('/[\r\n]/', $value);
}

var_dump(isSafeHeaderValue('asha@example.com'));
var_dump(isSafeHeaderValue("asha@example.com\r\nBcc: victim@example.com"));

Output

bool(true)
bool(false)

Additionally, validate email addresses with filter_var(..., FILTER_VALIDATE_EMAIL), which rejects line breaks. Mail libraries validate and encode headers automatically — another reason to use them.


Sending With Symfony Mailer

Symfony Mailer is a modern, widely used mail library (also used by Laravel). It supports SMTP and the APIs of major email providers.

composer require symfony/mailer
<?php
require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

$transport = Transport::fromDsn(getenv('MAILER_DSN'));   // e.g. smtp://user:pass@smtp.example.com:587
$mailer = new Mailer($transport);

$email = (new Email())
    ->from(new Address('no-reply@gocourse.in', 'GoCourse'))
    ->to(new Address('asha@example.com', 'Asha'))
    ->replyTo('support@gocourse.in')
    ->subject('Your order #501 is confirmed')
    ->text("Hi Asha,\n\nThank you for your order #501.\n")
    ->html('<p>Hi Asha,</p><p>Thank you for your order <strong>#501</strong>.</p>')
    ->attachFromPath(__DIR__ . '/invoices/invoice-501.pdf');

$mailer->send($email);

What This Does

  • The DSN (connection string) holds the server, port, username, and password. It is read from an environment variable so credentials never appear in code (see $_ENV).
  • text() and html() create a multipart message: email clients that cannot display HTML show the text version, and spam filters treat messages with both parts more favorably.
  • Attachments, encoding, and header formatting are handled by the library.
  • send() throws a TransportExceptionInterface exception if sending fails, which you can catch and log.

PHPMailer is another popular, long-established library with similar capabilities:

$mail = new PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASSWORD');
$mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->CharSet = 'UTF-8';
$mail->setFrom('no-reply@gocourse.in', 'GoCourse');
$mail->addAddress('asha@example.com', 'Asha');
$mail->Subject = 'Welcome to GoCourse';
$mail->Body = '<p>Welcome!</p>';
$mail->AltBody = 'Welcome!';
$mail->send();

Building Message Content

Keep email content in functions or templates, separate from the sending code. This makes it easy to preview and test:

<?php
function buildOrderConfirmation(array $order): array
{
    $lines = [];
    $rows = '';
    $total = 0;

    foreach ($order['items'] as $item) {
        $lineTotal = $item['qty'] * $item['price'];
        $total += $lineTotal;

        $lines[] = sprintf('- %s x %d: ₹%s', $item['name'], $item['qty'], number_format($lineTotal, 2));
        $rows .= sprintf(
            '<tr><td>%s</td><td>%d</td><td>₹%s</td></tr>',
            htmlspecialchars($item['name']),
            $item['qty'],
            number_format($lineTotal, 2)
        );
    }

    $text = "Hi {$order['customer']},\n\n"
        . "Thank you for your order #{$order['id']}.\n\n"
        . implode("\n", $lines) . "\n\n"
        . 'Total: ₹' . number_format($total, 2) . "\n\n"
        . "Team GoCourse\n";

    $html = '<p>Hi ' . htmlspecialchars($order['customer']) . ',</p>'
        . "<table>$rows</table>"
        . '<p><strong>Total: ₹' . number_format($total, 2) . '</strong></p>';

    return ['subject' => "Order #{$order['id']} confirmed", 'text' => $text, 'html' => $html];
}

$message = buildOrderConfirmation([
    'id' => 501,
    'customer' => 'Asha',
    'items' => [
        ['name' => 'Gel Pen', 'qty' => 10, 'price' => 20],
        ['name' => 'Notebook <A5>', 'qty' => 3, 'price' => 60],
    ],
]);

echo "Subject: {$message['subject']}\n\n" . $message['text'];

Output

Subject: Order #501 confirmed

Hi Asha,

Thank you for your order #501.

- Gel Pen x 10: ₹200.00
- Notebook <A5> x 3: ₹180.00

Total: ₹380.00

Team GoCourse

Key Points

  • The text version is plain text, so values are not HTML-escaped.
  • In the HTML version, every value is escaped with htmlspecialchars(): product names and customer names are data and could contain HTML (here <A5> becomes <A5>).
  • The function returns data only; sending is a separate step. This makes it possible to preview emails in the browser or test them without sending.

Email HTML is rendered by many different clients (Gmail, Outlook, mobile apps) with limited CSS support: use simple layouts, tables, and inline styles, or a dedicated email template tool.


Deliverability: Getting Into the Inbox

MeasurePurpose
SPF DNS recordLists the servers allowed to send mail for your domain
DKIM signatureCryptographically signs messages, proving they were not altered
DMARC DNS recordTells receivers how to handle mail failing SPF/DKIM, and requests reports
Matching From domainSend from your own domain, through a provider authorized in SPF/DKIM
Transactional email providerServices such as Amazon SES, Postmark, SendGrid, or Mailgun maintain sending reputation
Plain-text alternative and honest subject linesLower spam scores
Unsubscribe links (for newsletters)Required by many providers and laws

Without SPF, DKIM, and DMARC, major providers such as Gmail increasingly reject or spam-folder messages, especially from domains that send in volume.


Sending Email Reliably

  • Do not send during the web request when possible. Sending can take seconds or fail temporarily. Put the email into a queue (a database table or a queue system), and let a background worker send it with retries. Frameworks provide queued mail out of the box.
  • Log each sent email (recipient, type, status) without logging sensitive content.
  • Rate-limit forms that trigger emails (contact forms, password resets) to prevent abuse.
  • Never let users choose arbitrary recipients for messages sent from your domain.

Testing Without Sending Real Emails

Local tools such as Mailpit run a fake SMTP server (for example, on port 1025) and show captured messages in a web interface. Point your development MAILER_DSN to it (smtp://localhost:1025) to see exactly what would be sent, without emailing real people.


Common Mistakes

Relying on mail() in Production

Effect: Messages silently fail or land in spam.

Fix: Use a mail library with an SMTP server or provider API.

Putting User Input Into Headers Unchecked

Effect: Header injection turns your server into a spam relay.

Fix: Validate addresses and reject line breaks; let libraries build headers.

Unescaped Values in HTML Emails

Effect: Broken layouts and injected content in messages.

Fix: Escape all dynamic values in the HTML part.

Storing SMTP Credentials in Code

Effect: Credentials leak through version control.

Fix: Use environment variables.

Sending Synchronously in Every Request

Effect: Slow pages and lost emails when the mail server is unavailable.

Fix: Queue emails and send them in the background with retries.


Best Practices

  • Send through an authenticated SMTP server or email provider using Symfony Mailer or PHPMailer.
  • Provide both HTML and plain-text versions, and escape dynamic HTML content.
  • Configure SPF, DKIM, and DMARC for your sending domain.
  • Queue emails, log their status, and rate-limit email-triggering forms.
  • Test with a local mail catcher before sending to real users.

Official reference: https://www.php.net/manual/en/function.mail.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about Sending Email and get answers drawn from it.

Signed-in readers only.