PHPackages                             snicco/better-wp-mail - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. snicco/better-wp-mail

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

snicco/better-wp-mail
=====================

Keep your sanity when working with mails in WordPress

v1.10.1(1y ago)816.6k3LGPL-3.0-onlyPHPPHP ^7.4|^8.0

Since Apr 17Pushed 1y ago1 watchersCompare

[ Source](https://github.com/snicco/better-wp-mail)[ Packagist](https://packagist.org/packages/snicco/better-wp-mail)[ RSS](/packages/snicco-better-wp-mail/feed)WikiDiscussions master Synced 1mo ago

READMEChangelogDependencies (4)Versions (35)Used By (3)

BetterWPMail - The long overdue upgrade to `wp_mail`
====================================================

[](#betterwpmail---the-long-overdue-upgrade-to-wp_mail)

[![codecov](https://camo.githubusercontent.com/a99e6ec528fffd1664e95534f9a09a4a09d2afe62799ff0d8774dc22d8453f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436f7665726167652d3130302532352d73756363657373)](https://codecov.io/gh/snicco/snicco)[![Psalm Type-Coverage](https://camo.githubusercontent.com/c12cfed65c7da16501f7a84e7861b8c4757fc30e9dc00bb2983783dbb3f3f84c/68747470733a2f2f73686570686572642e6465762f6769746875622f736e6963636f2f736e6963636f2f636f7665726167652e7376673f)](https://shepherd.dev/github/snicco/snicco)[![Psalm level](https://camo.githubusercontent.com/c5e90ffcf3a5aa1f78f93bddde5db7627b114329393aa87697df8cedc7f5391a/68747470733a2f2f73686570686572642e6465762f6769746875622f736e6963636f2f736e6963636f2f6c6576656c2e7376673f)](https://psalm.dev/)[![PhpMetrics - Static Analysis](https://camo.githubusercontent.com/364ffb28ea219affd0fed2e99cc046bac0bf41da3f1d3814e0cbe4a4bb54c994/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5068704d6574726963732d5374617469635f416e616c797369732d326561343466)](https://snicco.github.io/snicco/phpmetrics/BetterWPMail/index.html)[![PHP-Versions](https://camo.githubusercontent.com/241a10d25aa09d5e8a82ebd2b55780a63dd43736d958d4004c3166e650874aca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253545372e34253743253545382e30253743253545382e312d626c7565)](https://camo.githubusercontent.com/241a10d25aa09d5e8a82ebd2b55780a63dd43736d958d4004c3166e650874aca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253545372e34253743253545382e30253743253545382e312d626c7565)

**BetterWPMail** is a small library that provides an expressive, object-orientated API around the [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/) function.

**BetterWPMail** is not an SMTP-plugin!

It has (optional) support for many mail transports, but will default to using a [`WPMailTransport`](src/Transport/WPMailTransport.php), so that it's usable in distributed **WordPress** code.

Table of contents
-----------------

[](#table-of-contents)

1. [Motivation](#motivation)
2. [Installation](#installation)
3. [Creating a mailer](#creating-a-mailer)
4. [Creating and sending emails](#creating-and-sending-emails)
    1. [Immutability](#immutability)
    2. [Sending an email](#sending-an-email)
    3. [Adding addresses](#adding-addresses)
    4. [Setting mail content](#setting-mail-content)
    5. [Adding context to templates](#adding-context-to-templates)
    6. [Adding attachments](#adding-attachments)
    7. [Embedding images](#embedding-images)
    8. [Adding custom headers](#adding-custom-headers)
    9. [Configuring emails globally](#configuring-emails-globally)
    10. [Extending the email class](#extending-the-email-class)
    11. [Using mail events](#using-mail-events)
    12. [Writing emails in markdown / Using a custom renderer](#writing-emails-in-markdown--using-a-custom-mailrenderer)
    13. [Handling exceptions](#handling-exceptions)
5. [Testing](#testing)
6. [Contributing](#contributing)
7. [Issues and PR's](#reporting-issues-and-sending-pull-requests)
8. [Security](#security)

Motivation
----------

[](#motivation)

To list all problems of the [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/) function would take a long time. The most problematic ones are:

- ❌ No support for a plain-text version when sending a html body.
- ❌ No support for inline-attachments.
- ❌ No support for complex multi-part emails.
- ❌ You can't choose a custom filename for attachments.
- ❌ You can't send attachments that you already have in memory (like a generated PDF). You always have to write to a tmp file first.
- ❌ Zero error-handling.
- ❌ No support for templated emails.
- ...
- ...

Many plugins employ massive hacks to circumvent these issues:

This is what you probably find in most **WordPress** plugin code:

```
function my_plugin_send_mail(string $to, string $html_message) {

    add_filter('phpmailer_init', 'add_plain_text');

    /* Add ten other filters */
    wp_mail($to, $html_message);

    remove_filter('phpmailer_init', 'add_plain_text')

    /* Remove ten other filters */
}

function add_plain_text(\PHPMailer\PHPMailer\PHPMailer $mailer) {
    $mailer->AltBody = strip_tags($mailer->Body);
}
```

**Why is this so bad?**

Besides the fact that you are running a lot of unneeded hooks for every email you sent, what happens if `wp_mail`throws an exception that is recovered somewhere else?

You now have ten leftover hook callbacks that modify every outgoing email during the same **PHP** process. Depending on the kind of filters you added, there is now great potential for bugs that are almost impossible to debug.

A real example of this can be seen here in the [WooCommerce](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/emails/class-wc-email.php#L647)code base. (Not bashing WooCommerce here, there is currently no alternative with the way `wp_mail` works.)

Under the hood **WordPress** uses the bundled [PHPMailer](https://github.com/PHPMailer/PHPMailer) which is a reputable and rock-solid library. [PHPMailer](https://github.com/PHPMailer/PHPMailer) has native support for most of the problems listed above, [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/)just doesn't use them.

Here is where **BetterWPMail** comes into play.

Installation
------------

[](#installation)

```
composer require snicco/better-wp-mail
```

Creating a `Mailer`
-------------------

[](#creating-a-mailer)

Instead of using [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/) directly, you'll use the [`Mailer`](src/Mailer.php) class which is able to send [`Email`](src/ValueObject/Email.php) objects.

Quickstart:

```
use Snicco\Component\BetterWPMail\Mailer;

$mailer = new Mailer();
```

The full signature of `Mailer::__construct` is

```
  public function __construct(
        ?Transport $transport = null,
        ?MailRenderer $mail_renderer = null,
        ?MailEvents $event_dispatcher = null,
        ?MailDefaults $default_config = null
    )
```

- [`Transport`](src/Transport/Transport.php) is an interface and will default to the [`WPMailTransport`](src/Transport/WPMailTransport.php) where all emails will eventually send using [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/).

    If you are using **BetterWPMail** in a controlled environment, you can provide your own implementation of the [`Transport`](src/Transport/Transport.php) interface. If you are distributing code you should always use the default transport since you can't control the SMTP-Plugin that your users will have installed.

    In the future we will create a [`symfony/mailer`](https://symfony.com/doc/current/mailer.html) transport which will allow you to send emails with any of dozens of providers that Symfony`s mailer integrates with.
- The [`MailRenderer`](src/Renderer/MailRenderer.php) interface is responsible for converting mail templates to html/plain-text content. By default, a [`FileSystemRenderer`](src/Renderer/FilesystemRenderer.php) will be used, which searches for a file matching the template name.
- The [`MailEvents`](src/Event/MailEvents.php) interface is responsible for firing events right before and right after an email was sent. By default, an instance of [`NullEvents`](src/Event/NullEvents.php) will be used which will not emit any events.
- [`MailDefaults`](src/ValueObject/MailDefaults.php) is responsible for providing fallback configuration for settings sender name, reply-to address etc.

Creating and sending emails
---------------------------

[](#creating-and-sending-emails)

### Immutability

[](#immutability)

The [`Email`](src/ValueObject/Email.php) class is an **immutable** value object. You can not change an email once its created. All public methods on the [`Email`](src/ValueObject/Email.php) class return a **new, modified version** of the object.

Immutability is not common in the PHP community, but it's actually simple to understand:

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = new Email();

❌ // This is incorrect.
$email->addTo('calvin@snicco.io');

✅ // This is correct
$email = $email->addTo('calvin@snicco.io');
```

The basic convention in **BetterWPMail** is:

- methods starting with `add` will merge attributes and return a **new** object.
- methods starting with `with` will replace attributes and return a **new** object.

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = new Email();

$email = $email->addTo('calvin@snicco.io');
// The email has one recipient now.

$email = $email->addTo('marlon@snicco.io');
// The email has two recipients now.

$email = $email->withTo('jondoe@snicco.io');
// The email has one recipient "jondoe@snicco.io"
```

---

### Sending an email

[](#sending-an-email)

Emails are sent using the [`Mailer`](#creating-a-mailer) class.

At minimum, an email needs a recipient and a body (html/text/attachments):

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = (new Email())->addTo('calvin@snicco.io')
                      ->withHtmlBody('BetterWPMail is awesome');

$mailer->send($email);
```

### Adding addresses

[](#adding-addresses)

All the methods that require email addresses (from(), to(), etc.) accept `strings`, `arrays`, a `WP_User` instance or a [`MailBox`](src/ValueObject/Mailbox.php) instance

```
use Snicco\Component\BetterWPMail\ValueObject\Email;
use Snicco\Component\BetterWPMail\ValueObject\Mailbox;

$email = new Email();

$admin = new WP_User(1);

$email = $email

    // email address is a simple string
    ->addTo('calvin@snicco.io')

    // with an explicit display name
    ->addCc('Marlon ')

    // as an array, where the first argument is the email
    ->addBcc(['Jon Doe', 'jon@snicco.io'])

    // as an array with a "name" + "email" key
    ->addFrom(['name' => 'Jane Doe', 'email' => 'jane@snicco.io'])

    // with an instance of WP_USER
    ->addFrom($admin)

    // with an instance of MailBox
    ->addReplyTo(Mailbox::create('no-reply@snicco.io'));
```

---

### Setting mail content

[](#setting-mail-content)

You have two options for setting the content of an email:

1. By setting it explicitly as a string.
2. By setting a template on the email object which will be rendered to html/plain-text before sending.

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = (new Email())->addTo('calvin@snicco.io');

$email = $email
    ->withHtmlBody('BetterWPMail is awesome')
    ->withTextBody('BetterWPMail supports plain text.')

$templated_email = $email
    ->withHtmlTemplate('/path/to/template-html.php')
    ->withTextBody('/path/to/template-plain.txt')
```

If an email has html-content but **no** explicit text-content, then the html-content will be passed through [`strip_tags`](https://www.php.net/manual/de/function.strip-tags.php) and be used as the plain-text version.

---

### Adding context to templates

[](#adding-context-to-templates)

Assuming that we want to send a welcome email to multiple users with the following template:

```

Hi ,

Thanks for signing up to
```

Here we can use the fact that emails are `immutable` to reuse a base email instance:

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = (new Email())

    ->withHtmlTemplate('path/to/email-templates/welcome.php')

    ->withContext(['site_name' => 'snicco.io']);

// Important: don't use withContext here or site_name is gone.
$email1 = $email->addContext('first_name', 'Calvin')
                ->addTo('calvin@snicco.io');

$mailer->send($email1);

$email2 = $email->addContext('first_name', 'Marlon');
                ->addTo('marlon@snicco.io');

$mailer->send($email2);
```

This will result in the following two emails being sent:

```
Hi Calvin,

Thanks for signing up to snicco.io
```

```
Hi Marlon,

Thanks for signing up to snicco.io
```

---

### Adding attachments

[](#adding-attachments)

Attachments can be added to an instance of [`Email`](src/ValueObject/Email.php) in two ways:

1. Attaching a local path on the filesystem.

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = (new Email())->addTo('calvin@snicco.io');

$email = $email

    ->addAttachment('/path/to/documents/terms-of-use.pdf')

    // optionally with a custom display name
    ->addAttachment('/path/to/documents/privacy.pdf', 'Privacy Policy')

    // optionally with an explicit content-type,
    ->addAttachment('/path/to/documents/contract.doc', 'Contract', 'application/msword');
```

2. Attaching a binary string or a stream that you already have in memory (a generated PDF for example)

```
use Snicco\Component\BetterWPMail\ValueObject\Email;

$pdf = /* generate pdf */

$email = (new Email())->addTo('calvin@snicco.io');

$email = $email

    ->addBinaryAttachment($pdf, 'Your PDF', 'application/pdf')
```

**BetterWPMail** depends on it's [`Transport` interface](src/Transport/Transport.php) to perform the actual sending of emails. For that reason, no mime-type detection is performed if you don't pass an explicit content-type for an attachment. This is delegated to the concrete transport implementation.

The [`WPMailTransport`](src/Transport/WPMailTransport.php) will delegate this task to `wp_mail`/`PHPMailer`. The behaviour of `PHPMailer` is the following:

1. If you pass an explicit mime-type, use that.
2. Try to guess the mime-type from the filename.
3. If 2. is not possible default to `application/octet-stream` which is defined as "arbitrary binary data".

---

### Embedding Images

[](#embedding-images)

If you want to display images inside your email, you must embed them instead of adding them as attachments.

In your email content you can then reference the embedded image with the syntax: `cid: + image embed name`

```

Hi ,

```

```
  use Snicco\Component\BetterWPMail\ValueObject\Email;

  $email = (new Email())
    ->addTo('calvin@snicco.io')
    ->addContext('first_name', 'Calvin');

  $email1 = $email
      ->addEmbed('/path/to/images/logo.png', 'logo', 'image/png')
      ->withHtmlTemplate('path/to/email-templates/welcome-with-image.php');

  // or with inline html
  $email2 = $email
      ->addEmbed('/path/to/images/logo.png', 'logo', 'image/png')
      ->withHtmlBody('');
```

---

### Adding custom headers

[](#adding-custom-headers)

```
  use Snicco\Component\BetterWPMail\ValueObject\Email;

  $email = (new Email())
    ->addTo('calvin@snicco.io')
    // custom headers are string, string key value pairs.
    // These are not validated in any form.
    ->addCustomHeaders(['X-Auto-Response-Suppress'=> 'OOF, DR, RN, NRN, AutoReply'])
```

---

### Configuring emails globally

[](#configuring-emails-globally)

The default configuration for all your emails is determined by the [`MailDefaults`](src/ValueObject/MailDefaults.php)class that you pass into the [`Mailer`](src/Mailer.php) class.

If you don't explicitly pass an instance of [`MailDefaults`](src/ValueObject/MailDefaults.php)when [creating your `Mailer`](#creating-a-mailer), they will be created based on the global **WordPress** settings.

Remember: You can always overwrite these settings on a per-email basis.

```
use Snicco\Component\BetterWPMail\Mailer;
use Snicco\Component\BetterWPMail\ValueObject\MailDefaults;

$from_name = 'My Plugin';
$from_email = 'myplugin@site.com';

$reply_to_name = 'My Plugin Reply-To'
$reply_to_email = 'myplugin-reply-to@site.com';

$mail_defaults = new MailDefaults(
    $from_name,
     $from_email,
     $reply_to_name,
     $reply_to_email
);

// Other arguments set to default for brevity.
$mailer = new Mailer(null, null, null, $mail_defaults);
```

---

### Extending the `Email` class

[](#extending-the-email-class)

If you are sending the same email in multiple places, you might want to extend the [`Email`](src/ValueObject/Email.php)class to preconfigure shared settings in one place.

Creating your custom emails classes has a lot of synergy with [mail events](#using-mail-events).

An example for a custom welcome email:

```
use Snicco\Component\BetterWPMail\ValueObject\Email;
use Snicco\Component\BetterWPMail\ValueObject\Mailbox;

class WelcomeEmail extends Email {

    // You can configure the protected
    // priorities of the Email class
    protected ?int $priority = 5;

    protected string $text = 'We would like to welcome you to snicco.io';

    protected ?string $html_template = '/path/to/templates/welcome.php';

    public function __construct(WP_User $user) {

        // configure dynamic properties in the constructor.
        $this->subject = sprintf('Welcome to snicco.io %s', $user->display_name);

        $this->to[] = Mailbox::create($user);

        $this->context['first_name'] = $user->first_name;

    }

}

$user = new WP_User(1);

$mailer->send(new WelcomeEmail($user));
```

---

### Using mail events

[](#using-mail-events)

When you call `Mailer::send` two types of events are fired.

Right before passing the [`Email`](src/ValueObject/Email.php) instance to the [configured `Transport`](#creating-a-mailer) the [`SendingEmail`](src/Event/SendingEmail.php) event is fired. This event contains the current [`Email`](src/ValueObject/Email.php) as a public property which gives you an opportunity to change its settings before sending.

Right after an email is sent the [`EmailWasSent`](src/Event/EmailWasSent.php) event is fired. This event is mainly useful for logging purposes.

To use mail events you have to pass an instance of [`MailEvents`](src/Event/MailEvents.php)when [creating your mailer instance](#creating-a-mailer).

By default, **BetterWPMail** comes with an [implementation](src/Event/MailEventsUsingWPHooks.php) of this interface that uses the [**WordPress** hook system](https://developer.wordpress.org/plugins/hooks/).

```
use Snicco\Component\BetterWPMail\Event\MailEventsUsingWPHooks;
use Snicco\Component\BetterWPMail\Event\SendingEmail;
use Snicco\Component\BetterWPMail\Mailer;
use Snicco\Component\BetterWPMail\Transport\WPMailTransport;

$mailer = new Mailer(
    null,
    null,
    new MailEventsUsingWPHooks()
);

add_filter(Email::class, function (SendingEmail $event) {
    // This will add 'admin@site.com' to every email that is being sent.
    $event->email = $event->email->addBcc('admin@site.com');
});

add_filter(WelcomeEmail::class, function (SendingEmail $event) {
    // This will add 'welcome@site.com' to every welcome email that is sent.
    $event->email = $event->email->addBcc('welcome@site.com');
});
```

A common use-case of mail events is allowing users to customize specific mails:

```
// In your code
$user = new WP_User(1);
$mailer->send(new MyPluginWelcomeMail($user));

// Third-party code:
add_filter(MyPluginWelcomeMail::class, function (SendingEmail $event) {
    // This will overwrite your default template for the "MyPluginWelcomeEmail" only
    $event->email = $event->email->withHtmlTemplate('path/to/custom/welcome.php');
});
```

### Writing emails in markdown / Using a custom `MailRenderer`

[](#writing-emails-in-markdown--using-a-custom-mailrenderer)

If you pass no arguments when [creating your mailer instance](#creating-a-mailer) the default renderer will be used which is a combination of:

- The [`AggregateRenderer`](src/Renderer/AggregateRenderer.php) (which delegates the rendering to between multiple [`MailRenderer`](src/Renderer/MailRenderer.php) instances)
- The [`FilesystemRenderer`](src/Renderer/FilesystemRenderer.php) (which looks for a file that matches the template name set on the [`Email`](src/ValueObject/Email.php#L28))

Let's now create a custom setup:

- We want to render markdown emails and
- Use the [`FilesystemRenderer`](src/Renderer/FilesystemRenderer.php) as a fallback.

First we need a way to convert markdown to HTML.

We will use [`erusev/parsedown`](https://github.com/erusev/parsedown) for this task.

```
composer require erusev/parsedown
```

Now let's create a custom `MarkdownMailRenderer`:

```
use Snicco\Component\BetterWPMail\Renderer\MailRenderer;

class MarkdownEmailRenderer implements MailRenderer {

    // This renderer should only render .md files that exist.
    public function supports(string $template_name,?string $extension = null) : bool{

        return 'md' === $extension && is_file($template_name);

    }

    public function render(string $template_name,array $context = []) : string{

        // First, we get the string contents of the template.
        $contents = file_get_contents($template_name);

        // To allow basic templating, replace placeholders inside {{ }}
        foreach ($context as $name => $value ) {
            $contents = str_replace('{{'.$name'.}}', $value);
        }

        // Convert the markdown to HTML and return it.
        return (new Parsedown())->text($contents);

    }

}
```

Now that we are ready to render markdown emails we can create our [`Mailer`](src/Mailer.php) like this:

```
use Snicco\Component\BetterWPMail\Mailer;
use Snicco\Component\BetterWPMail\Renderer\AggregateRenderer;
use Snicco\Component\BetterWPMail\Renderer\FilesystemRenderer;
use Snicco\Component\BetterWPMail\ValueObject\Email;

// This mail renderer will use our new markdown renderer (if possible) and default the filesystem renderer.
$mail_renderer = new AggregateRenderer(
    new MarkdownMailRenderer(),
    new FilesystemRenderer(),
);

$mailer = new Mailer(null, $mail_renderer);

$email = new Email();
$email = $email->addTo('calvin@snicco.io');

// This email will be renderer with the default renderer
$email_html = $email->withHtmlTemplate('/path/to/templates/welcome.php');
$mailer->send($email_html);

// This email will be renderer with our new markdown renderer.
$email_markdown= $email->withHtmlTemplate('/path/to/templates/markdown/welcome.md');
$mailer->send($email_markdown);
```

---

### Handling exceptions

[](#handling-exceptions)

In contrast to [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/), calling `Mailer::send()` will throw a [`CantSendEmail`](src/Exception/CantSendEmail.php) exception on failure.

```
use Snicco\Component\BetterWPMail\Exception\CantSendEmail;
use Snicco\Component\BetterWPMail\ValueObject\Email;

$email = (new Email())->addTo('calvin@snicco.io')
                      ->withHtmlBody('BetterWPMail has awesome error handling');

try {
    $mailer->send($email);
} catch (CantSendEmail $e) {

   // You can catch this exception if you like,
   // or let it bubble up depending on your use case.
    error_log($e->getDebugData());
}
```

This has numerous advantages over the native way of interacting with [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/):

```
function handleMailError(WP_Error $error) {
    // what now?
}

add_action('wp_mail_failed', 'handleMailError');

$success = wp_mail('calvin@snicco.io', 'wp_mail has bad error_handling');

remove_action('wp_mail_failed', 'handleMailError');

if($success === false) {
    // what now?
}
```

Testing
-------

[](#testing)

**BetterWPMail** comes with a dedicated testing package that provides a `FakeTransport` class that you should use during testing.

First, install the package as a composer `dev-dependency`:

```
composer install --dev snicco/better-wp-mail-testing
```

How you wire the `FakeTransport` into your [`Mailer`](src/Mailer.php) instance during testing greatly depends on how your overall codebase is set up. You probably want to do this inside your dependency-injection container.

The `FakeTranport` has the following **phpunit** assertion methods:

```
use Snicco\Component\BetterWPMail\Mailer;
use Snicco\Component\BetterWPMail\Testing\FakeTransport;
use Snicco\Component\BetterWPMail\ValueObject\Email;

$mailer = new Mailer($transport = new FakeTransport());

// This fill pass
$transport->assertNotSent(WelcomeEmail::class);

$mailer->send(new MyPluginWelcomeEmail());

// This will fail now.
$transport->assertNotSent(MyPluginWelcomeEmail::class);

// This will pass
$transport->assertSent(MyPluginWelcomeEmail::class);

// This will fail
$transport->assertSent(PurchaseEmail::class);

// This will fail
$transport->assertSentTimes(MyPluginWelcomeEmail:class, 2);

$mailer->send(new MyPluginWelcomeEmail());

// This will now pass.
$transport->assertSentTimes(MyPluginWelcomeEmail:class, 2);

$email = (new Email())->addTo('calvin@snicco.io');
$mailer->send($email);

// This will pass
$transport->assertSentTo('calvin@snicco.io');
// This will pass
$transport->assertNotSentTo('marlon@snicco.io');

$email = (new Email())->addTo('marlon@snicco.io');
$mailer->send($email);

// This will now fail.
$transport->assertNotSentTo('marlon@snicco.io');

// Using an assertion closure. This will pass.
$transport->assertSent(Email::class, function (Email $email) {
    return $email->to()->has('calvin@snicco.io')
});
```

**Intercepting WordPress emails**

In addition to faking emails send by your own code that uses the [Mailer](src/Mailer.php) class, the `FakeTransport`also lets you fake all other emails that are sent directly by using [`wp_mail`](https://developer.wordpress.org/reference/functions/wp_mail/).

```
use Snicco\Component\BetterWPMail\Testing\FakeTransport;
use Snicco\Component\BetterWPMail\Testing\WPMail;

$transport = new FakeTransport()

$transport->interceptWordPressEmails();

// This will pass
$transport->assertNotSent(WPMail::class);

// No emails will be sent here.
wp_mail('calvin@snicco.io', 'Hi calvin', 'Testing WordPress emails was never this easy...');

// This will now fail.
$transport->assertNotSent(WPMail::class);

// This will pass
$transport->assertSent(WPMail::class);

// This will pass
$transport->assertSent(WPMail::class, function (WPMail $mail) {
    return 'Hi calvin' === $mail->subject();
});

// This will fail
$transport->assertSent(WPMail::class, function (WPMail $mail) {
    return 'Hi marlon' === $mail->subject();
});
```

Contributing
------------

[](#contributing)

This repository is a **read-only** split of the development repo of the [**Snicco** project](https://github.com/snicco/snicco).

[This is how you can contribute](https://github.com/snicco/snicco/blob/master/CONTRIBUTING.md).

Reporting issues and sending pull requests
------------------------------------------

[](#reporting-issues-and-sending-pull-requests)

Please report issues in the [**Snicco** monorepo](https://github.com/snicco/snicco/blob/master/CONTRIBUTING.md##using-the-issue-tracker).

Security
--------

[](#security)

If you discover a security vulnerability within **BetterWPMail**, please follow our [disclosure procedure](https://github.com/snicco/snicco/blob/master/SECURITY.md).

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance35

Infrequent updates — may be unmaintained

Popularity25

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity65

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~27 days

Recently: every ~1 days

Total

33

Last Release

610d ago

Major Versions

v1.10.0 → v2.0.0-beta.12024-09-01

v1.10.1 → v2.0.0-beta.72024-09-04

### Community

Maintainers

![](https://www.gravatar.com/avatar/d659a021c9077ceb60e6ab1ea4287b9f209edb97e93bceceb7fb539d8302e894?d=identicon)[calvinalkan](/maintainers/calvinalkan)

---

Top Contributors

[![snicco-bot](https://avatars.githubusercontent.com/u/101470239?v=4)](https://github.com/snicco-bot "snicco-bot (25 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/snicco-better-wp-mail/health.svg)

```
[![Health](https://phpackages.com/badges/snicco-better-wp-mail/health.svg)](https://phpackages.com/packages/snicco-better-wp-mail)
```

###  Alternatives

[tijsverkoyen/css-to-inline-styles

CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.

5.8k505.3M228](/packages/tijsverkoyen-css-to-inline-styles)[minishlink/web-push

Web Push library for PHP

1.9k12.0M52](/packages/minishlink-web-push)[laravel-notification-channels/twilio

Provides Twilio notification channel for Laravel

2587.7M12](/packages/laravel-notification-channels-twilio)[spatie/url-signer

Generate a url with an expiration date and signature to prevent unauthorized access

4422.3M16](/packages/spatie-url-signer)[mattketmo/email-checker

Throwaway email detection library

2742.0M5](/packages/mattketmo-email-checker)[laravel-notification-channels/discord

Laravel notification driver for Discord.

2371.3M11](/packages/laravel-notification-channels-discord)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
