Pigeon Atlas
← Integrations

Send email from WordPress

WordPress hands every message to PHP's mail() and hopes. A forty-line must-use plugin sends them through Pigeon Atlas instead — password resets, WooCommerce receipts, form notifications — from your own domain.

Updated September 15, 2026

Out of the box WordPress sends mail with PHP's mail(), from whatever the web server happens to be, unsigned. On most hosting that lands in spam or nowhere, which is why "WordPress is not sending emails" is one of the most-searched sentences in the platform's history. The usual answer is an SMTP plugin. This one is an HTTP plugin you write yourself in forty lines, and it needs no SMTP account.

Everything in WordPress that sends mail — password resets, new-user notices, WooCommerce order emails, Contact Form 7, Gravity Forms — calls wp_mail(). Since 5.7 that function offers pre_wp_mail, a filter that lets you take the message and send it your own way.

The plugin

Create wp-content/mu-plugins/pigeonatlas-mail.php. Must-use plugins load before everything else and cannot be deactivated from the admin by accident.

<?php
/**
 * Plugin Name: Pigeon Atlas mail
 * Description: Sends every wp_mail() through Pigeon Atlas over HTTP.
 */

add_filter('pre_wp_mail', function ($null, $atts) {
    $to = is_array($atts['to']) ? $atts['to'] : array_map('trim', explode(',', $atts['to']));
    $headers = is_array($atts['headers'] ?? []) ? $atts['headers'] : explode("\n", (string) ($atts['headers'] ?? ''));
    $is_html = false;
    foreach ($headers as $header) {
        if (stripos($header, 'content-type:') === 0 && stripos($header, 'text/html') !== false) {
            $is_html = true;
        }
    }

    $body = [
        'from'    => get_bloginfo('name') . ' <' . PIGEONATLAS_FROM . '>',
        'to'      => $to[0],
        'subject' => $atts['subject'],
        ($is_html ? 'html' : 'text') => $atts['message'],
    ];

    $response = wp_remote_post('https://pigeonatlas.com/v1/emails', [
        'timeout' => 15,
        'headers' => [
            'Authorization' => 'Bearer ' . PIGEONATLAS_API_KEY,
            'Content-Type'  => 'application/json',
        ],
        'body' => wp_json_encode($body),
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 400) {
        error_log('Pigeon Atlas: ' . (is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_body($response)));
        return false;
    }

    return true;
}, 10, 2);

Then in wp-config.php, above the "That's all, stop editing" line:

define('PIGEONATLAS_API_KEY', 'pa_live_your_key_here');
define('PIGEONATLAS_FROM', 'hello@mail.yoursite.com');

Returning true from the filter tells WordPress the message was handled and stops it from trying mail() as well; false reports a failure to whatever called wp_mail(), which is what WooCommerce and the form plugins expect.

What this changes for plugins

Nothing on their side. WooCommerce still builds its order emails, Contact Form 7 still sends its notifications; they call wp_mail() as before and it goes out through us. The From address they set is ignored on purpose — every message leaves from the address you defined, on the domain you verified, because that is the only address the API will accept. If a plugin sets a Reply-To header, add it to $body['reply_to'] from $headers in the same way as the content type.

Multiple recipients and attachments

The API sends one message per recipient; the plugin above sends to the first address. If a plugin passes several, loop over $to and post once each. Attachments arrive in $atts['attachments'] as file paths and can be sent as base64 — the API reference shows the field — but a receipt with a PDF is the exception in WordPress, and the forty lines above cover the rest.

Before it works

Add mail.yoursite.com in Pigeon Atlas and publish the DKIM, SPF and MX records it shows. Use a subdomain rather than the bare domain: the site's password resets and a marketing list should never share a reputation. Until the three records resolve, sends are refused with a message naming the missing record — and after they do, the messages page shows every one WordPress sent, with what happened to it, which mail() never could.

A thousand a month free, no card

Enough to verify a domain and run a small app from it.

Start sending

Other platforms