How to Send Emails in PHP in 2023

January 23, 2023
Written by
Reviewed by

How to Send Emails in PHP in 2022

Email! Yes. Email.

Despite what you might think, it is still one of the most used and trusted methods of communication. This might seem strange, given that it was invented back in the 60s and 70s. However, it's estimated that around 333 billion emails were sent in 2022. What's more, that figure's estimated to grow to over 376 billion by the end of 2025.

Why's this important — even interesting — you may be asking. After all, the cool kids are on Instagram, TikTok, and so many other social media channels. I won't deny that social media does get a lot of attention, and if you're looking for reach and engagement, perhaps they should be your focus.

But if you're looking for the greatest ROI (Return On Investment), a platform that isn't subject to changing algorithms, one that is more personal, and one that people significantly trust, then email is still the best choice!

So, in this article, you're going to learn three different ways to send emails with PHP; a combination of both classic and modern approaches.

Prerequisites

Code overview

Each email will contain the same properties:

  • A reply to address, a from address, and three recipients (one direct, one on CC, and one on BCC)
  • The subject line "Sending emails with PHP is FUN!"
  • A plain text and HTML body
  • Two attachments, a text file and a PDF file

By doing this, none of the three approaches will have an unfair advantage over any other, and you'll be able to see what is required in each implementation.

Create the project directory structure

Start off by creating the project's directory structure and switch into it, by running the commands below.

mkdir -p send-email-with-php/{data,templates/emails}
cd send-email-with-php

If you're using Microsoft Windows, use the commands below instead.

mkdir send-email-with-php
mkdir send-email-with-php\data
mkdir send-email-with-php\templates
mkdir send-email-with-php\templates\emails
cd send-email-with-php

Download the supporting files to send as attachments

Now that the project's directory structure's in place, download the two files which each email will send as attachments (text.txt and text.pdf) to the data directory.

Install the dependencies

Next, install the required dependencies; these are:

DependencyDescription
PHP DotenvPHP Dotenv populates PHP's $_SERVER and $_ENV superglobals from a configuration file. This encourages keeping sensitive configuration details out of code and version control.
Symfony MailerThis package is a feature-rich and very flexible way to create and send emails, having support for multipart messages, file attachments plus lots more.
The Official Twilio SendGrid PHP API LibraryThis package simplifies sending emails with the Twilio SendGrid Web API with PHP.
The Symfony Mailer SendGrid TransportThis package allows Symfony Mailer to send emails using SendGrid's API.
The Symfony Twig BundleThis package provides Twig support when sending emails using Symfony's Mailer component.
TukioTukio is an implementation of the PSR-14 Event Dispatcher specification. It will be used to know if an email was sent successfully or not when working with Symfony Mailer.

To install them, run the command below.

composer install crell/tukio \
    sendgrid/sendgrid \
    symfony/mailer \
    symfony/sendgrid-mailer \
    symfony/twig-bundle \
    vlucas/phpdotenv

If you're using Microsoft Windows, replace the backslash (\) at the end of each line with a caret (^).

Create a SendGrid API key

The next thing to do is to create and retrieve your SendGrid API key. To do that, log in to your SendGrid account and under Settings > API Keys click Create API Key. In the form that appears, enter a meaningful name for the key in the API Key Name field and click Create & View.

Create a SendGrid API key

The SendGrid API key will now be visible. Copy and paste it into .env in place of the two <<SENDGRID_API_KEY>> placeholders.

Copy your SendGrid API key

Be aware that you will only be able to view the API key once. If you navigate away from the page or close the tab/window and haven't copied the API key, you will have to create a new one.

Set the required environment variables

You next need to set two environment variables so that the code can interact with SendGrid's API; these are

  • MAILER_DSN: This contains all the information required for Symfony Mailer to instantiate a Transport class to send the email with
  • SENDGRID_API_KEY: This is required to make authenticated requests with SendGrid

To set them, create a new file named .env in the project's top-level directory and paste the configuration below.

MAILER_DSN=sendgrid://<<SENDGRID_API_KEY>>@default
SENDGRID_API_KEY="<<SENDGRID_API_KEY>>"

Write the PHP code

Now, it's time to create the code to send emails with PHP.

Send an email with pure PHP

Let's start off by sending an email using PHP's mail() function. To do that, create a new file in the top-level directory of the project named php-mailer.php and paste the below code into it.

<?php

declare(strict_types=1);

const MAX_LINE_LENGTH = 79;

function addAttachment(string $filename, string $boundWithPre): string {
    if (! file_exists($filename) || ! is_readable($filename)) {
        throw new Exception(sprintf('%s either does not exist or is not readable.', $filename));
    }

    $message = $boundWithPre;
    $message .= sprintf(
        "\nContent-Type: %s; name=\"%s\"",
        mime_content_type($filename),
        basename($filename)
    );
    $message .= "\nContent-Transfer-Encoding: base64\n";
    $message .= "\nContent-Disposition: attachment\n";
    $message .= chunk_split(base64_encode(file_get_contents($filename)));

    return $message;
}

$headers = [
    'Cc' => 'User 2 <cc.user@example.org>',
    'Bcc' => 'User 3 <bcc.user@example.org>',
    'Reply-To' => 'User 4 <reply.to.user@example.org>',
    'From' => 'User 5 <from.user@example.org>',
    'X-Mailer' => sprintf("PHP %s", phpversion()),
    'MIME-Version' => '1.0',
    'Content-type' => 'text/html; charset=UTF-8',
];

$htmlBody = <<<EOF
<h1>Sending emails with PHP is FUN!</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.</p>
<p>Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.</p>
<p>
    <a href="https://example.org">Click here</a> to learn more.
</p>
EOF;

$plainBody = <<<EOF
Sending emails with PHP is FUN!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.

Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.
    
Go to https://example.org to learn more.
EOF;

$boundary  = sprintf("PHP-mixed-%s", md5((string)time()));
$boundWithPre  = "\n--".$boundary;
$message   = $boundWithPre;
$message  .= "\nContent-Type: text/html; charset=UTF-8\n";
$message  .= "\n" . wordwrap($htmlBody, MAX_LINE_LENGTH, "\r\n");

$message  .= $boundWithPre;
$message  .= "\nContent-Type: text/plain; charset=UTF-8\n";
$message  .= "\n$plainBody";

$attachments = [
        __DIR__ . "/data/text.txt",
        __DIR__ . "/data/text.pdf"
];
foreach ($attachments as $attachment) {
    try {
        $message .= addAttachment($attachment, $boundWithPre);
    } catch (Exception $e) {
        printf(
            'Could not attach [%s] to the email message. Reason: %s',
            $attachment,
            $e->getMessage()
        );
        exit(-1);
    }
}

$message .= $boundWithPre."--";

$status = mail(
    "User 1 <recipient.to@example.org>",
    "Sending emails with PHP is FUN!",
    $message,
    $headers,
);

echo (bool)$status
    ? "Email was successfully sent"
    : "Email was NOT successfully sent";

Near the top of the code, replace all of the email addresses in the $headers array, with valid addresses that you have access to.

Now, let's step through the code. It starts off by defining a constant that holds the maximum line length of a MIME message, which is 79 chars. Then, it defines a function named addAttachment(), which will add a MIME attachment to the email's message body.

The function checks if the file to be attached exists and is readable. If so, it then sets a MIME boundary pre-marker, the attachment's content type, content transfer encoding, and content disposition, before attaching the contents of the file to the message's body.

The contents are Base64-encoded so that all of the information will survive sending from sender to receiver, and then split into smaller chunks as necessitated by RFC 2045.

Then, it sets a series of headers, specifying the email's sender and reply-to addresses, along with any recipients on CC and BCC, adds the plain and HTML message body, and attaches the two files. Finally, it sends the email and prints out whether or not the message was sent successfully.

Send an email with the code

With the code written, send an email with it by running the command below.

php php-mailer.php

Known issues with PHP's mail function

Before you move on to the second implementation, it's worth covering a few things about PHP's mail function.

While it is readily available in any PHP installation, it's not necessarily the best choice for sending emails either reliably or efficiently. Among other reasons, this is because:

  • Mail servers may not accept emails if they're from servers that are not properly configured or if emails are not formatted correctly
  • Generally speaking, you have to be familiar with a number of RFCs, such as:
    • 1896 (The text/enriched MIME Content-type)
    • 2045 (Format of Internet Message Bodies)
    • 2049 (Multipurpose Internet Mail Extensions)
    • 2822 (Internet Message Format)
  • SMTP has different considerations to Sendmail implementations
  • The mail() function is not suitable for large email volumes in a loop, as it opens and closes an SMTP socket for each email.
  • A lot of manual work and background knowledge is potentially required, which dedicated libraries handle for you. This includes knowing that
    • Messages should be separated with a CRLF (\r\n).
    • Message line length should not be larger than 80 characters.
    • When PHP is talking to an SMTP server directly (Windows only) if a full stop is found at the start of a line, it is removed.
    • Retrieving error information when an email isn't successfully sent is different on Windows than on Linux/OSX.
  • See the notes in the PHP manual for more information.

It may be quick and simple to use and available by default with any PHP installation, but there are often better choices, such as Symfony's Mailer component.

Did you know that the first "visual" email client was called Laurel and it ran on the Xerox Alto computer? However, in terms of a modern GUI client, the first was Eudora, invented in 1988 by Steve Dorner.

Here is what both looked like:

Screenshots of Eudora and Laurel email clients

Image credit: https://www.outlooktransfer.com/take-a-look-at-how-the-first-ever-email-client-looked-like/ and https://www.file-extensions.org/imgs/app-picture/9553/eudora-for-mac.png.

Send an email with Symfony's Mailer component

Now, let's work through an example of how to send an email with Symfony Mailer. To do that, create a new file in the project's top-level directory, named symfony-mailer.php. In the new file, paste the code below.

<?php

use Crell\Tukio\Dispatcher;
use Crell\Tukio\OrderedListenerProvider;
use Symfony\Bridge\Twig\Mime\BodyRenderer;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Event\FailedMessageEvent;
use Symfony\Component\Mailer\Event\SentMessageEvent;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

require_once('vendor/autoload.php');

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

$textAttachment = __DIR__ . '/data/text.txt';
$PDFAttachment = __DIR__ . '/data/text.pdf';
$email = (new TemplatedEmail())
    ->replyTo('User 4 <reply.to.user@example.org>')
    ->from(new Address('from.user@example.org, 'User 5'))
    ->to(new Address('recipient.user@example.org', 'User 1'))
    ->cc(new Address('cc.user@example.org', 'User 2'))
    ->bcc(new Address('bcc.user@example.org', 'User 3'))
    ->subject('Sending emails with PHP is FUN!')
    ->attach(
        file_get_contents($textAttachment),
        basename($textAttachment),
        'application/text'
    )
    ->attach(
        file_get_contents($PDFAttachment),
        basename($PDFAttachment),
        'application/text'
    )
    ->text('text.body.twig')
    ->html('html.body.twig');

$email
    ->getHeaders()
    ->addTextHeader('X-Mailer', sprintf("PHP %s", phpversion()));

$provider = new OrderedListenerProvider();
$provider->addListener(function(FailedMessageEvent $event) {
    printf("Email failed to send because %s\n", $event->getError()->getMessage());
});
$provider->addListener(function(SentMessageEvent $event) {
    printf("Email id is %s\n", $event->getMessage()->getMessageId());
});
$dispatcher = new Dispatcher($provider);

$transport = Transport::fromDsn($_SERVER['MAILER_DSN'], $dispatcher);
$mailer = new Mailer($transport);
try {
    $mailer->send($email);
} catch (TransportExceptionInterface $e) {
    sprintf("Email could not be sent. Reason: %s\n", $e->getMessage());
}

The code starts by loading the variables in .env as variables in PHP's $_SERVER and $_ENV superglobals. After that, it initialises two variables, $textAttachment and $PDFAttachment with the paths to two files to attach to the email.

Then, it initialises a new TemplatedEmail object ($email) which will store the email's properties; this object is required as the email will send a plain text and HTML body.

On the object, it:

  • Sets the reply to, sender, and recipient addresses
  • Sets the subject
  • Attaches the text and PDF files
  • Sets the text and HTML message bodies from two templates; which will be defined shortly

Then, a PSR-14 Event Dispatcher is initialised. This is so that a confirmation of the email being sent successfully or details as to why the email was unable to be sent, can be printed to the console. The dispatcher adds a listener to two events that Symfony Mailer fires:

  • A FailedMessageEvent: This is fired when an email cannot be sent for some reason. If one is dispatched, the reason for the failure to send is printed to the terminal.
  • A SentMessageEvent: This is fired when an email was successfully sent.

After that, a new Transport object ($transport) is initialised from the MAILER_DSN environment variable that was set in .env. The DSN instructs the code that it will use the underlying SendGrid Transport, and provides your SendGrid API key so that authenticated requests can be made successfully to SendGrid's API.

Symfony's Mailer component supports several built-in and 3rd party transports, such as Sendmail, SMTP, Gmail, Postmark, and Amazon SES. I've used SendGrid as it's one of the two I'm most familiar with. If you're familiar with another service or prefer Sendmail or SMTP, please update the DSN accordingly.

A Mailerobject ($mailer), which handles sending the message with $transport is then initialised.

Create the HTML body template

The plain text and HTML message bodies are generated from Twig templates, so let's create the templates, starting with the one for the HTML body.

Create a new file named html.body.twig in template/emails and in it, paste the code below.

<h1>Sending emails with PHP is FUN!</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.</p>
<p>Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.</p>
<p>
    <a href="https://example.org">Click here</a> to learn more.
</p>

It's not that special, just a small HTML file that includes a header and three paragraphs, the final one with a link.

Create the text body template

Next, create a new file named text.body.twig in template/emails and in it, paste the code below.

Sending emails with PHP is FUN!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.

Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.

Go to https://example.org to learn more.

It's the plain text version of the previous HTML template.

Send an email with the code

Now that the code's written, send an email with it by running the command below.

php symfony-mailer.php

Did you know that the first email was sent by Ray Tomlinson in 1971? He sent a message to himself and received it on a computer sitting right next to him.

Ray Tomlinson, inventor of email

Image credit: https://i.dawn.com/primary/2016/03/56dd2a645a251.jpg.

Send an email with SendGrid's Library for PHP

Now, let's work through one final example of how to send an email, this time with SendGrid's Library for PHP. To do that, create a new file in the project's top-level directory, named sendgrid-mailer.php. In the new file, paste the code below.

<?php

require_once('vendor/autoload.php');

use SendGrid\Mail\Mail;
use SendGrid\Mail\ReplyTo;
use SendGrid\Mail\To;

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

$htmlBody = <<<EOF
<h1>Sending emails with PHP is FUN!</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.</p>
<p>Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.</p>
<p>
    <a href="https://example.org">Click here</a> to learn more.
</p>
EOF;

$plainBody = <<<EOF
Sending emails with PHP is FUN!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus venenatis volutpat orci. Quisque in interdum purus. Sed et lorem consequat, hendrerit sapien vel, gravida turpis. Fusce facilisis sodales dui. Pellentesque pellentesque efficitur orci in imperdiet. Vivamus vulputate, nulla a vestibulum pharetra, massa eros bibendum ex, vel faucibus nisl libero eget lectus.

Maecenas egestas molestie convallis. Nullam aliquam mattis ipsum, at dictum tortor tempus vitae. Ut in sem sagittis, elementum elit non, luctus augue. Cras eleifend ligula risus, eu vulputate velit porttitor porttitor. Curabitur eu consectetur ante, eu gravida neque.
    
Go to https://example.org to learn more.
EOF;

$email = new Mail();
$email->setFrom('from.user@example.org', 'User 5');
$email->setSubject('Sending with Twilio SendGrid is Fun');
$email->addTo('recipient.user@example.org', 'User 1');
$email->addBcc('bcc.user@example.org', 'User 3');
$email->addCc('cc.user@example.org', 'User 2');
$email->setReplyTo(
    new ReplyTo(
        'reply.to@example.org',
        'User 4'
    )
);
$email->addContent(
    'text/plain',
    $plainBody
);
$email->addContent(
    'text/html',
    $htmlBody
);

$textAttachment = __DIR__ . '/data/text.txt';
$PDFAttachment = __DIR__ . '/data/text.pdf';
foreach ([$textAttachment, $PDFAttachment] as $attachment) {
    $email->addAttachment(
        base64_encode(
            file_get_contents($attachment)
        ),
        mime_content_type($attachment),
        basename($attachment),
        'attachment'
    );
}

$sendgrid = new SendGrid($_SERVER['SENDGRID_API_KEY']);
try {
    $response = $sendgrid->send($email);
    printf("Status code: %d.\n", $response->statusCode());
} catch (Exception $e) {
    echo 'Caught exception: '.  $e->getMessage(). "\n";
}

The code starts off by loading the variables in .env as environment variables in PHP's $_SERVER and $_ENV superglobals. Then, it initialises two variables, one to hold the message's HTML body ($htmlBody) and the other to hold the plain text body ($plainBody).

Following that, it initialises a Mail object ($email) which stores the email's properties. On it, as with the Symfony Mailer implementation, it stores the sender and recipients email addresses, the reply to address, and subject. It also sets the message's plain text and HTML body, before adding the two attachments ($textAttachment, $PDFAttachment).

After that, it finishes up by initialising a new SendGrid object ($sendgrid) with your SendGrid API key and attempts to send the email. If the response from SendGrid was a success, then the status code, and response's headers and body are printed to the terminal. If the email failed to send, the reason why is printed to the terminal.

Send an email with the code

Now that the code's written, send an email with it by running the command below.

php sendgrid-mailer.php

All being well, you should see output, similar to the following printed to the terminal.

Status code: 202.

From the output, you can see that the status code is 202, indicating that SendGrid accepted the email and will send it at some point in the future.

Did you know that the first web-based email service was called WebMail and that HoTMaiL (which became Windows Live Mail, then Windows Live Hotmail, and finally Outlook.com) was the first commercial web-based email service?

Screenshot of HotMail running in Internet Explorer

Image credit: https://i.ytimg.com/vi/0bKVgz6ueqA/maxresdefault.jpg

How to debug email-sending problems

While it's great when emails arrive as expected. For quite a large number of reasons, such as anti-spam filters, SMTP servers being blacklisted, and SPF validation failure, this doesn't always happen.

So before you finish up this tutorial, let's look at a series of ways to help you debug emails that failed to arrive.

Use the mail command

When sending mail with a combination of PHP's mail() function and a local Sendmail daemon or SMTP server, a quick way to find out why an email failed to send is the mail command.

If you're using Microsoft Windows, the email command bundled with Cygwin should provide, more or less, the same functionality.

From the terminal, type mail and press Enter. You will see output similar to the example below:

Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/settermjd": 2 messages 2 unread
>U  1 MAILER-DAEMON@C02D10  Wed Dec  7 13:20 108/4535  "Undelivered Mail Returned to Sender"
 U  2 MAILER-DAEMON@C02D10  Wed Dec 14 13:50 123/5604  "Undelivered Mail Returned to Sender"

The subject of the two messages ("Undelivered Mail Returned to Sender") gives a broad indication of what went wrong. To look at one of the messages, press the number that precedes it, e.g., 1 or 2. You'll then see output similar to the example below.

I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.

For further assistance, please send mail to postmaster.

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

        The mail system

<recipient.user@example.org>: host gmail-smtp-in.l.google.com[108.177.126.27]
        said: 550-5.7.25 [87.123.245.2] The IP address sending this message does
        not have a 550-5.7.25 PTR record setup, or the corresponding forward DNS
        entry does not 550-5.7.25 point to the sending IP. As a policy, Gmail does
        not accept messages 550-5.7.25 from IPs with missing PTR records. Please
        visit 550-5.7.25  https://support.google.com/mail/answer/81126#ip-practices
        for more 550 5.7.25 information.
        dm22-20020a170907949600b007aea9dfb4f5si11163055ejc.511 - gsmtp (in reply to
        end of DATA command)

<recipient.user@example.org>: host aspmx.l.google.com[2a00:1450:4013:c07::1a]
        said: 550-5.7.1 [2001:9e8:33cc:cf00:81f0:545c:12ea:963] Our system has
        detected that 550-5.7.1 this message does not meet IPv6 sending guidelines
        regarding PTR 550-5.7.1 records and authentication. Please review 550-5.7.1
        https://support.google.com/mail/?p=IPv6AuthError for more information 550
        5.7.1 . bo27-20020a0564020b3b00b004677a9c4b39si8247880edb.91 - gsmtp (in
        reply to end of DATA command)

The message shows the specific reason why the email was not sent, about halfway down:

> The IP address sending this message does not have a 550-5.7.25 PTR record setup, or the corresponding forward DNS entry does not 550-5.7.25 point to the sending IP. As a policy, Gmail does not accept messages 550-5.7.25 from IPs with missing PTR records.

Use the SendGrid Dashboard

The Suppressions section of the SendGrid dashboard

If you're sending emails with SendGrid's API, log in to the SendGrid Dashboard. Then, in the left-hand side navigation menu, expand the Suppressions section. In that section, you can find four subsections (Bounces, Spam Reports, Blocks, and Invalid) that may help determine why an email failed to arrive.

Look at SendGrid's response body

If you're using SendGrid, you can use the response body to view email formatting or validation errors and, potentially, how to fix them.

To do that, in sendgrid-mailer.php, update the try/catch block, at the end of the file, to match the following.


try {
    $response = $sendgrid->send($email);
    print $response->statusCode() . "\n";
    print_r($response->headers());
    print $response->body() . "\n";
} catch (Exception $e) {
    echo 'Caught exception: '.  $e->getMessage(). "\n";
}

Now, if you run the script again, and an email fails to send, then you'll see output similar to the following, printed to the terminal.

{
    "errors": [{
        "message": "Each email address in the personalization block should be unique between to, cc, and bcc. We found the first duplicate instance of [cc.user@example.org] in the personalizations.0.bcc field.",
        "field": "personalizations.0",
        "help": "http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.recipient-errors"
    }]
}

There, you can see the reason why the email wasn't sent in the message element, and a link to documentation that should be able to help you resolve the issue in the help element.

You could serialise the error response body into a custom PHP object to make it easier to work with. Alternatively, you could use the excellent jq to filter out information from the response that you aren't interested in.

For example, the following command example would only print the message element.


php sendgrid-mailer.php |  jq '.errors[].message'

That's three ways to send emails with PHP in 2023

While there are many ways to send an email with PHP in 2023, some methods are more efficient and reliable than others. I hope you'll continue exploring the functionality of both Symfony Mailer and SendGrid's PHP API Library. They're two excellent, feature-rich packages.

Matthew Setter is a PHP Editor in the Twilio Voices team and a PHP, Go, and Rust developer. He’s also the author of Mezzio Essentials and Docker Essentials. When he's not writing PHP code, he's editing great PHP articles here at Twilio. You can find him at msetter@twilio.com, and on Twitter, and GitHub.

Honeywell ad image used in the main post image via https://www.globalnerdy.com/wordpress/wp-content/uploads/2009/04/homeywell-electronic-mail-ad.jpg.