PHPackages                             jardisadapter/mailer - 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. jardisadapter/mailer

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

jardisadapter/mailer
====================

SMTP mail client with STARTTLS, authentication, HTML and plain text support, attachments, and retry with exponential backoff; a building block of the open-source foundation that Jardis-generated DDD code runs on

v1.0.3(2w ago)0110↓83.3%1MITPHPPHP &gt;=8.2CI passing

Since Jun 2Pushed 2w agoCompare

[ Source](https://github.com/jardisAdapter/mailer)[ Packagist](https://packagist.org/packages/jardisadapter/mailer)[ Docs](https://jardis.io)[ RSS](/packages/jardisadapter-mailer/feed)WikiDiscussions main Synced today

READMEChangelog (4)Dependencies (8)Versions (6)Used By (1)

Jardis Mailer
=============

[](#jardis-mailer)

[![Build Status](https://github.com/jardisAdapter/mailer/actions/workflows/ci.yml/badge.svg)](https://github.com/jardisAdapter/mailer/actions/workflows/ci.yml/badge.svg)[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE.md)[![PHP Version](https://camo.githubusercontent.com/a68b290dcc313d698dc138a1111aa83eee2f143605449d7e8b5416ea6f88558f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253345253344382e322d3737374242342e737667)](https://www.php.net/)[![PHPStan Level](https://camo.githubusercontent.com/c51bda247654363d3e30bc352674dd761a9557803a14af0226eb411d6dc0006b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d4c6576656c253230382d627269676874677265656e2e737667)](phpstan.neon)[![PSR-12](https://camo.githubusercontent.com/34b10db0caa29bacd49bda5c437a8de95385f036f3230b31fa605326e18da22c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436f64652532305374796c652d5053522d2d31322d626c75652e737667)](phpcs.xml)[![Zero Dependencies](https://camo.githubusercontent.com/858b349f67cfc7a4ba5ae2b152efa5d10592fb9a948239c8675dc002639a94ff/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446570656e64656e636965732d5a65726f2a2d627269676874677265656e2e737667)](#)

> Part of **[Jardis](https://jardis.io)** — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.

**Transactional emails without the bloat.** A lean SMTP mailer for PHP built on raw sockets — covers order confirmations, password resets, and notifications. No Swiftmailer, no Symfony Mailer, no dependency tree. Just SMTP over a socket.

\* Zero external PHP packages. Only `ext-openssl` + `ext-mbstring` + `jardissupport/contract` (interfaces only).

---

Why This Mailer?
----------------

[](#why-this-mailer)

- **Two classes are enough** — `Mailer` + `SmtpConfig`, nothing else to learn
- **Fluent message builder** — immutable, `with*` pattern like PSR-7
- **STARTTLS + implicit SSL** — secure by default, PORT 587 or 465
- **AUTH LOGIN &amp; PLAIN** — standard SMTP authentication
- **HTML + plain text** — multipart/alternative, just works
- **File attachments &amp; inline images** — Base64 encoded, Content-ID for HTML embedding
- **Retry with backoff** — automatic retry on connection errors and temporary SMTP failures
- **Connection keepalive** — send 100 emails over one SMTP connection
- **NOOP health-check** — stale connections are silently reconnected
- **96% test coverage** — integration tests against a real SMTP server, not mocks

---

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

[](#installation)

```
composer require jardisadapter/mailer
```

---

Quick Start
-----------

[](#quick-start)

### Send a Simple Email

[](#send-a-simple-email)

```
use JardisAdapter\Mailer\Mailer;
use JardisAdapter\Mailer\Config\SmtpConfig;
use JardisAdapter\Mailer\Data\MailMessage;

$mailer = new Mailer(new SmtpConfig(
    host: 'smtp.example.com',
    username: 'user@example.com',
    password: 'secret',
));

$message = MailMessage::create()
    ->withFrom('noreply@example.com', 'My App')
    ->withTo('customer@example.com', 'Jane Doe')
    ->withSubject('Your Order Confirmation')
    ->withText('Thank you for your order #1234.')
    ->withHtml('Thank you!Your order #1234 has been confirmed.');

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

### HTML + Plain Text

[](#html--plain-text)

```
$message = MailMessage::create()
    ->withFrom('noreply@example.com')
    ->withTo('user@example.com')
    ->withSubject('Weekly Report')
    ->withText('Your weekly report is attached.')
    ->withHtml('Weekly ReportSee attachment.');
```

Both bodies are sent as `multipart/alternative` — the recipient's mail client picks the best one.

### Attachments

[](#attachments)

```
$message = MailMessage::create()
    ->withFrom('billing@example.com')
    ->withTo('customer@example.com')
    ->withSubject('Your Invoice')
    ->withText('Please find your invoice attached.')
    ->withAttachment(file_get_contents('invoice.pdf'), 'invoice.pdf', 'application/pdf')
    ->withAttachment($csvData, 'report.csv', 'text/csv');
```

### Inline Images in HTML

[](#inline-images-in-html)

```
$message = MailMessage::create()
    ->withFrom('news@example.com')
    ->withTo('subscriber@example.com')
    ->withSubject('Our Newsletter')
    ->withHtml('News')
    ->withEmbeddedImage(file_get_contents('logo.png'), 'logo.png', 'image/png');
```

### Multiple Recipients, CC, BCC

[](#multiple-recipients-cc-bcc)

```
$message = MailMessage::create()
    ->withFrom('team@example.com')
    ->withTo('alice@example.com', 'Alice')
    ->withTo('bob@example.com', 'Bob')
    ->withCc('manager@example.com')
    ->withBcc('archive@example.com')
    ->withReplyTo('support@example.com')
    ->withSubject('Meeting Notes')
    ->withText('Notes from today.');
```

### Custom Headers

[](#custom-headers)

```
$message = MailMessage::create()
    ->withFrom('alerts@example.com')
    ->withTo('admin@example.com')
    ->withSubject('Server Alert')
    ->withText('CPU at 95%')
    ->withHeader('X-Priority', '1')
    ->withHeader('X-Mailer', 'Jardis Mailer');
```

---

Fully Configured
----------------

[](#fully-configured)

```
$mailer = new Mailer(new SmtpConfig(
    host: 'smtp.example.com',
    port: 587,                    // Default: 587
    encryption: 'tls',            // 'tls' (STARTTLS), 'ssl' (implicit), 'none'
    username: 'user@example.com',
    password: 'secret',
    timeout: 30,                  // Connect + read/write timeout in seconds
    fromAddress: 'noreply@example.com',  // Default From (applied when not set on message)
    fromName: 'My Application',
    maxRetries: 3,                // Retry on connection errors and 4xx
    retryDelayMs: 200,            // Exponential backoff: 200ms, 400ms, 800ms
));
```

---

Batch Sending
-------------

[](#batch-sending)

Send multiple emails over a single SMTP connection — the connection stays alive between messages:

```
$messages = [];
foreach ($recipients as $recipient) {
    $messages[] = MailMessage::create()
        ->withFrom('noreply@example.com')
        ->withTo($recipient->email, $recipient->name)
        ->withSubject('Your monthly statement')
        ->withHtml($renderer->render($recipient));
}

$result = $mailer->sendBatch($messages);

echo $result->successCount() . ' sent, ' . $result->failureCount() . ' failed';

foreach ($result->failed() as $failure) {
    log($failure['message']->to(), $failure['error']->getMessage());
}
```

---

Retry
-----

[](#retry)

```
$mailer = new Mailer(new SmtpConfig(
    host: 'smtp.example.com',
    maxRetries: 3,          // Up to 3 retries
    retryDelayMs: 200,      // Exponential backoff: 200ms, 400ms, 800ms
));
```

Automatically retries on `SmtpConnectionException` and temporary SMTP errors (4xx). No retry on permanent errors (5xx) — those are recipient or policy errors.

---

Error Handling
--------------

[](#error-handling)

ExceptionWhen`SmtpConnectionException`Host unreachable, TLS handshake failure, timeout`SmtpAuthenticationException`LOGIN or PLAIN rejected`SmtpTransportException`SMTP protocol error (rejected recipient, DATA error)`MailMessageException`Invalid message (missing From, To, or body)All implement `MailerExceptionInterface` for generic catch:

```
use JardisSupport\Contract\Mailer\MailerExceptionInterface;

try {
    $mailer->send($message);
} catch (MailerExceptionInterface $e) {
    // Any mailer error
}
```

---

Encryption
----------

[](#encryption)

PortEncryptionHow it works587`tls` (default)Connects plain, upgrades via STARTTLS465`ssl`Connects over implicit TLS25`none`No encryption (not recommended)---

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

[](#architecture)

The user only sees `Mailer` + `SmtpConfig` + `MailMessage`. Internally, the mailer orchestrates a pipeline of invokable handlers — built from the config:

```
Mailer (Orchestrator)
  │
  │  Transformers (MailMessage → MailMessage, built from config):
  │  ├── DefaultFrom          apply default sender if not set
  │  └── MessageValidator     validate before sending
  │
  │  Encoder (MailMessage → Envelope):
  │  └── MimeEncoder          MIME assembly, Base64, Quoted-Printable, RFC 2047
  │
  │  Transport (Envelope → void):
  │  └── SmtpTransport        socket-based SMTP with NOOP health-check
  │
  │  Retry (internal to Mailer):
  │  └── Exponential backoff on connection errors and 4xx
  │
  ▼
  send():
    foreach transformer → $message = $transform($message)
    $envelope = $encoder($message)
    $transport($envelope)        // with retry

```

Each handler is an **invokable object** (`__invoke`) — independently testable, replaceable, composable. Only what is configured gets instantiated.

### Custom Transport

[](#custom-transport)

The transport is a closure — replaceable for testing or alternative delivery:

```
$mailer = new Mailer(
    config: new SmtpConfig(host: 'localhost'),
    transport: function (Envelope $envelope): void {
        // Log, mock, or send via API
        file_put_contents('/tmp/mail.log', $envelope->rawMessage);
    },
);
```

---

Jardis Foundation Integration
-----------------------------

[](#jardis-foundation-integration)

In a Jardis DDD project, the mailer is automatically configured via ENV:

```
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=user@example.com
MAIL_PASSWORD=secret
MAIL_TIMEOUT=30
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME=My Application
```

The `MailerHandler` in `JardisApp` builds the mailer and registers it in the ServiceRegistry. Your application code receives `MailerInterface` via injection — without ever importing `Mailer` directly.

---

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

[](#development)

```
cp .env.example .env    # One-time setup
make install             # Install dependencies
make phpunit             # Run tests
make phpstan             # Static analysis (Level 8)
make phpcs               # Coding standards (PSR-12)
```

---

Documentation
-------------

[](#documentation)

Full documentation, guides, and API reference:

**[docs.jardis.io/en/adapter/mailer](https://docs.jardis.io/en/adapter/mailer)**

---

License
-------

[](#license)

[MIT License](LICENSE.md) — free for any use, including commercial.

AI-Assisted Development
-----------------------

[](#ai-assisted-development)

This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:

```
composer require --dev jardis/dev-skills
```

More details:

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance95

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 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 ~3 days

Total

4

Last Release

20d ago

### Community

Maintainers

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

---

Top Contributors

[![Headgent](https://avatars.githubusercontent.com/u/245725954?v=4)](https://github.com/Headgent "Headgent (7 commits)")

---

Tags

domain-driven-designemailhexagonal-architecturejardismailerphpsmtpstarttlsphpmailemailmailerDomain Driven Designsmtphexagonal-architecturetransactional emailHeadgentjardisSTARTTLS

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jardisadapter-mailer/health.svg)

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

PHPackages © 2026

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