All posts
3 min readPigeonify

How to send email with PHP

Send transactional email from PHP without mail() — using cURL against a REST API or PHPMailer over SMTP, with error handling that works.

phpapismtptutorial

PHP ships with mail(), and it is the reason a great deal of PHP email never arrives. It hands the message to a local sendmail binary and returns true if that binary accepted it — which tells you nothing about whether the message was delivered, authenticated, or silently dropped by the receiving server.

If you remember one thing: mail() returning true is not evidence of anything.

Before you start

You need three things, and they take about five minutes:

  1. An account. Sign up free — 5,000 emails a month, no card.
  2. A verified sending domain. Add your domain in the dashboard and publish the DKIM and SPF records it gives you. You cannot send from a domain you have not proved you control, and neither can anyone pretending to be you — that is the whole point. If the terms are unfamiliar, SPF, DKIM and DMARC explained covers them properly.
  3. An API key. Create one under API Keys. Treat it like a password: it can send mail as you.

Put the key in your environment rather than your source. PHP reads it from there in every example below.

With cURL

No dependencies at all:

<?php

function sendEmail(array $payload): array
{
    $curl = curl_init("https://www.pigeonify.com/api/v1/send");

    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json",
            "X-API-Key: " . getenv("PIGEONIFY_API_KEY"),
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $error = curl_error($curl);
    curl_close($curl);

    if ($body === false) {
        throw new RuntimeException("Email request failed: {$error}");
    }

    $decoded = json_decode($body, true);

    if ($status >= 400) {
        throw new RuntimeException("Email rejected ({$decoded['code']}): {$decoded['error']}");
    }

    return $decoded;
}

sendEmail([
    "from" => "you@yourdomain.com",
    "to" => "customer@example.com",
    "subject" => "Welcome aboard",
    "html" => "<p>Thanks for signing up.</p>",
]);

Set CURLOPT_TIMEOUT. Without it a hung connection blocks the PHP-FPM worker, and enough of those blocks the site.

With PHPMailer

If you are already using PHPMailer, only the transport changes:

<?php

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

$mail = new PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host = "smtp.pigeonify.com";
    $mail->SMTPAuth = true;
    $mail->Username = getenv("PIGEONIFY_SMTP_USER");
    $mail->Password = getenv("PIGEONIFY_SMTP_PASS");
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port = 587;

    $mail->setFrom("you@yourdomain.com", "Your Product");
    $mail->addAddress("customer@example.com");
    $mail->isHTML(true);
    $mail->Subject = "Welcome aboard";
    $mail->Body = "<p>Thanks for signing up.</p>";
    $mail->AltBody = "Thanks for signing up.";

    $mail->send();
} catch (Exception $e) {
    error_log("Email failed: {$mail->ErrorInfo}");
}

Set AltBody. HTML-only messages are treated with more suspicion by filters, and the plain-text part is one line.

Why not just fix mail()?

You can configure a local MTA to relay through an authenticated provider, and plenty of hosts do. But you inherit a queue you have to monitor, a log you have to read, and no visibility into bounces unless you build it. Calling an API gives you a message id, a delivery webhook and a dashboard, for the same amount of code.

What to get right before production

Do not send from a request handler and wait. An HTTP call to any email provider can take a second or more, and a slow provider becomes a slow checkout page. Queue the send and return.

Handle the failure. A send can fail because the address is wrong, because you are rate limited, or because the provider is having a bad minute. The first is permanent and the other two are not — retrying a dead address forever is how a sender's reputation degrades. See how to handle bounces.

Use a subdomain. Send transactional mail from mail.yourdomain.com rather than the domain that carries your business correspondence. If sending reputation is ever damaged, the damage stays contained.

Never send to an address that has bounced or unsubscribed. Suppression should be automatic, not a thing you remember. Pigeonify suppresses permanent bounces and complaints for you and refuses to send to them afterwards.

Where to go next

If you are weighing providers, we publish honest comparisons that say where the other product is better.

Send email that actually arrives

Authentication, suppression and bounce handling built in. Start free, no card required.

Start free