PHPackages                             phpdot/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. phpdot/mail

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

phpdot/mail
===========

Coroutine-safe transactional email for the PHPdot ecosystem: a fluent, immutable message builder over any symfony/mailer transport.

v1.0.0(today)00MITPHPPHP &gt;=8.3

Since Jun 13Pushed todayCompare

[ Source](https://github.com/phpdot/mail)[ Packagist](https://packagist.org/packages/phpdot/mail)[ RSS](/packages/phpdot-mail/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (2)Used By (0)

phpdot/mail
===========

[](#phpdotmail)

Coroutine-safe transactional email for the PHPdot ecosystem. Compose a message with a fluent, immutable builder and send it through any transport — **SMTP**, **sendmail**, or any Symfony transport — from one injectable service:

```
$mail->to('alice@example.com')->subject('Welcome')->html($body)->send();
```

Delivery is delegated to the battle-tested [`symfony/mailer`](https://github.com/symfony/mailer) + `symfony/mime`, fenced entirely behind a single transport boundary — the rest of the package (the builder, the value objects, the receipt) is plain PHPdot code. A fresh transport is built per send, so concurrent coroutines never share a socket under Swoole.

Install
-------

[](#install)

```
composer require phpdot/mail
```

RequirementVersionPHP&gt;= 8.3symfony/mailer^7.0symfony/mime^7.0phpdot/packageoptional — auto-wiring + `config/mail.php` scaffolding (with phpdot/container, phpdot/config)Quick Start
-----------

[](#quick-start)

```
use PHPdot\Mail\Mailer;
use PHPdot\Mail\MailConfig;
use PHPdot\Mail\Transport\EmailFactory;
use PHPdot\Mail\Transport\Transport;

// In an app you inject Mailer — see "DI Wiring". Wired by hand here:
$mail = new Mailer(
    new MailConfig(dsn: 'smtp://user:pass@smtp.example.com:587', fromEmail: 'no-reply@example.com'),
    new Transport(new EmailFactory()),
);

$receipt = $mail
    ->to('alice@example.com', 'Alice')
    ->subject('Welcome aboard')
    ->html('Hi Alice')
    ->text('Hi Alice')
    ->send();

echo $receipt->messageId; // ''
```

> **Every message needs a sender.** Set `fromEmail` in config (as above) so chains can omit `->from()` — otherwise call `->from('you@example.com', 'You')` on the message.

Why phpdot/mail
---------------

[](#why-phpdotmail)

- **Sends, nothing more.** The mailer's job is delivery. An HTML body is just a string — render it with `phpdot/template`, a heredoc, or anything else, and pass it to `->html()`. Zero coupling to a template engine.
- **Fluent and immutable.** `$mail->to(...)->subject(...)->send()` reads top to bottom. Each step returns a *new* `Message`, so chaining off the shared mailer never mutates it, and a configured base message is a safe reusable template.
- **Coroutine-safe.** The mailer is a stateless singleton and a fresh transport (its own socket) is built per send, so concurrent sends under Swoole never share a connection.
- **One dependency, well-fenced.** Symfony is reached only through the `Transport/` boundary. Every Symfony failure is translated into the package's own `MailException` / `TransportException`, so no Symfony type leaks into your code.
- **Honest about delivery.** `send()` returns a `Receipt` (message id) when the transport accepts the message and throws when it's rejected — and the docs are explicit that *accepted is not delivered* (see [Outcomes](#outcomes)).
- **Strict.** `declare(strict_types=1)` throughout, PHPStan level 10 with strict rules, zero ignored errors.

Architecture
------------

[](#architecture)

```
src/
├── Mailer.php              #[Singleton] #[Binds] — inject this; compose a message
├── MailConfig.php          #[Config('mail')] — transport DSN + default sender
├── Receipt.php             the outcome of an accepted send (message id + debug)
├── Contract/
│   └── MailerInterface.php
├── Message/
│   ├── Message.php         immutable fluent builder; send() delivers it
│   ├── Mailbox.php         a validated address (email + name)
│   └── Attachment.php      a file on disk or raw bytes in memory
├── Transport/
│   ├── Transport.php       builds a per-send transport from the DSN, delivers
│   └── EmailFactory.php    maps a Message onto a symfony/mime Email
└── Exception/
    ├── MailException.php       base — catch this for anything from the package
    └── TransportException.php  the transport could not deliver

```

Flow: inject `Mailer` → `$mail->to(...)` starts a fresh `Message` → `->send()` hands it to `Transport`, which maps it to a MIME email (`EmailFactory`), builds a one-shot transport from the DSN, delivers, and returns a `Receipt`. Symfony lives only inside `Transport/`.

Composing a Message
-------------------

[](#composing-a-message)

A message is built fluently and immutably — start it from the mailer (`$mail->to(...)`) or with `$mail->message()`, then chain:

```
$message = $mail->message()
    ->from('no-reply@example.com', 'Acme')
    ->to('alice@example.com', 'Alice')
    ->cc('manager@example.com')
    ->bcc('audit@example.com')
    ->replyTo('support@example.com')
    ->subject('Your invoice')
    ->text('Plain-text fallback')
    ->html('Rich HTML body')
    ->attach('/path/invoice.pdf', 'invoice.pdf')
    ->priority(1)
    ->header('X-Campaign', 'invoices');

$receipt = $message->send();
```

`Message` builder`from` · `to` · `cc` · `bcc` · `replyTo` `(email, name = '')`Addresses — validated on the spot.`subject(string)`Subject line.`html(string)` · `text(string)`HTML body · plain-text part (set both for multipart).`attach(path, name = null)`Attach a file from disk.`attachData(bytes, name, contentType = null)`Attach raw bytes already in memory.`priority(int)`1 (highest) … 5 (lowest).`header(name, value)`A custom header.`send(): Receipt`Deliver the message.Every setter returns a new `Message`, so `$base = $mail->from('no-reply@acme.com', 'Acme')` is a reusable template you branch per recipient.

Sending
-------

[](#sending)

When you don't need to hold the message, chain straight through to `send()`:

```
$receipt = $mail->to('alice@example.com')->subject('Hi')->html($body)->send();
```

The shortcuts (`from`/`to`/`cc`/`bcc`/`replyTo`/`subject`/`html`/`text`) live on `MailerInterface`, so the chain works through the injected contract, not just the concrete class.

Outcomes
--------

[](#outcomes)

`send()` tells you two things, and deliberately cannot tell you a third:

OutcomeHow you get it**Accepted** — the transport took the messagea `Receipt` (`messageId` + the SMTP `debug` transcript)**Rejected** — connection / auth / recipient / format failurea thrown `TransportException` / `MailException`**Delivered, bounced, or spam-filtered***not knowable here* — only your provider's webhooks report it```
use PHPdot\Mail\Exception\TransportException;

try {
    $receipt = $message->send();
    $logger->info('mail accepted', ['id' => $receipt->messageId]);
} catch (TransportException $e) {
    $logger->error('mail rejected', ['why' => $e->getMessage()]);
}
```

**"Accepted" means handed off to the mail server, not delivered to the inbox.** Bounces and spam filtering happen asynchronously — keep the `Receipt`'s `messageId` to correlate this send with the delivery and bounce webhooks your provider sends later.

Transports
----------

[](#transports)

The transport is chosen by the DSN in config — anything `symfony/mailer` supports:

DSNTransport`smtp://user:pass@host:587`SMTP`sendmail://default`local sendmail`native://default`the MTA configured in `php.ini``null://null`discard — the default, sends nowhereProvider APIs (SES, Postmark, Mailgun, …) work too: install the matching `symfony/*-mailer` bridge and use its DSN.

Configuration
-------------

[](#configuration)

`MailConfig` is the typed config, scaffolded into `config/mail.php` by `phpdot/package`:

KeyDefault`dsn``null://null`Transport DSN — read it from `MAIL_DSN`.`fromEmail``''`Default sender, used when a message sets no `from`.`fromName``''`Default sender display name.```
// config/mail.php  (generated once; edit freely)
return [
    'dsn'       => env('MAIL_DSN', 'smtp://localhost:1025'),
    'fromEmail' => env('MAIL_FROM', 'no-reply@example.com'),
    'fromName'  => 'Acme',
];
```

DI Wiring
---------

[](#di-wiring)

`Mailer` is `#[Singleton]` and `#[Binds(MailerInterface::class)]`, `MailConfig` is `#[Config('mail')]`, and the transport pieces are `#[Singleton]` — so with `phpdot/package` everything autowires and `config/mail.php` is scaffolded. Nothing to register.

```
use PHPdot\Mail\Contract\MailerInterface;

final class WelcomeController
{
    public function __construct(private MailerInterface $mail) {}

    public function register(string $email, string $body): void
    {
        $this->mail->to($email)->subject('Welcome')->html($body)->send();
    }
}
```

It is stateless (each `send()` builds its own transport), so the single shared instance is **coroutine-safe** under Swoole.

Development
-----------

[](#development)

```
composer test      # PHPUnit (Unit + Integration)
composer analyse   # PHPStan level 10, strict rules
composer cs-check  # php-cs-fixer dry run
composer check     # all three
```

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

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

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Tags

emailmailmailerphpsmtpswoolemailemailmailerswoolesmtpphpdot

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phpdot-mail/health.svg)

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

###  Alternatives

[nette/mail

📧 Nette Mail: A handy library for creating and sending emails in PHP.

49110.1M268](/packages/nette-mail)[railsware/mailtrap-php

The Mailtrap SDK provides methods for all API functions.

58879.6k](/packages/railsware-mailtrap-php)[yiisoft/mailer-symfony

Adapter for `yiisoft/mailer` relying on `symfony/mailer`

1384.8k2](/packages/yiisoft-mailer-symfony)[dotkernel/dot-mail

Dotkernel mail component based on symfony mailer

1143.5k6](/packages/dotkernel-dot-mail)

PHPackages © 2026

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