Self‑hosted transactional & notification email service for PHP. It stores projects, campaigns, templates and recipients in a relational database, renders HTML emails from reusable template blocks, sends them over SMTP (PHPMailer), and tracks opens, clicks and (un)subscriptions through a small set of HTTP endpoints.
The public surface is a plain PHP library (
Xakki\Emailer\Emailer) plus an HTTP front controller and a console runner. There is no framework lock‑in — you wire it into your own app, container, or the bundled Docker stack.
- Projects → campaigns → templates → queue domain model on top of Doctrine DBAL 4.
- Template engine with
{{placeholder}}substitution, reusable wrapper / content / block templates and per‑project parameters. - SMTP delivery via PHPMailer with DKIM support and SMTP error classification (spam / quota / invalid mailbox / temporary, …).
- Open & click tracking: tracking pixel, link rewriting, and per‑recipient statistics.
- Subscription management: one‑click subscribe / unsubscribe endpoints,
List-Unsubscribeheader. - HTTP API (Phroute router) and a console runner for queue processing and migrations.
- Redis caching for MX lookups and auth tokens; Doctrine Migrations for schema.
- Tested on PHP 8.4 and 8.5, static‑analysed with PHPStan (level 7) and PSR‑12 (strict).
- PHP >= 8.4 with extensions:
pdo,pdo_mysql,json,fileinfo,redis,intl,mbstring - A MySQL / MariaDB database
- Redis
- Composer 2
composer require xakki/emailerRun the database migrations (against your configured connection):
./console migrations migrateConfiguration is a plain PHP array passed to ConfigService. Only db.password
is strictly required; everything else has sane defaults (see
src/ConfigService.php).
use Xakki\Emailer\ConfigService;
$config = new ConfigService([
'db' => [
'driver' => 'pdo_mysql',
'host' => '127.0.0.1',
'port' => 3306,
'user' => 'emailer',
'password' => 'a-strong-unique-password', // required
'dbname' => 'emailer',
],
'redis' => ['host' => '127.0.0.1', 'port' => 6379],
// Optional: enables the read-only GET /emailer/get/{key}/{secret} accessor.
'secret_key' => getenv('SECRET_EMAILER_KEY') ?: '',
]);| Key | Default | Notes |
|---|---|---|
db |
pdo_mysql localhost set |
Doctrine DBAL connection params; password required |
redis |
emailer-redis:6379 |
Used for MX / auth caches |
route |
built‑in tracking routes | Phroute route → [Controller, method] map |
migration |
src/Migration |
Doctrine Migrations config |
secret_key |
'' (disabled) |
Guards the read‑only body accessor |
use Monolog\Logger;
use Xakki\Emailer\Emailer;
use Xakki\Emailer\Model\Template;
use Xakki\Emailer\Transports\Smtp;
$emailer = new Emailer($config, new Logger('emailer'));
// 1. One-time setup: project, templates, transport, notify channel, campaign.
$project = $emailer->createProject('My project', [
Template::NAME_HOST => 'mail.example.com',
Template::NAME_ROUTE => '/emailer',
Template::NAME_LANG => 'en',
Template::NAME_URL_LOGO => __DIR__ . '/tpl/logo.png',
]);
$wrapper = $project->createTplWrapper('Base', file_get_contents('tpl/wrapper.html'));
$content = $project->createTplContent('News', file_get_contents('tpl/content.html'));
$notify = $project->createNotify('Newsletter');
$smtp = new Smtp($emailer);
$smtp->fromEmail = 'robot@example.com';
$smtp->fromName = 'Robot';
$smtp->host = 'smtp.example.com';
$smtp->port = 587;
$project->createTransport($smtp);
$campaign = $project->createCampaign('Welcome {{name}}', $wrapper, $content, $notify, []);
// 2. Queue a message for a recipient.
$mail = $emailer->getNewMail()
->setEmail('user@example.com')
->setEmailName('Jane Doe')
->setData(['name' => 'Jane']);
$hashRoute = $emailer
->getNewSender($campaign->project_id, $campaign->id)
->send($mail);A runnable end‑to‑end example lives in example/as-vendor/init.php.
./console send # send pending messages from the queue
./console newDay # reset per-day transport counters (run daily via cron)
./console migrations migrateThe front controller (Emailer::dispatchRoute()) exposes the tracking surface.
Default routes (see ConfigService::$route):
| Method & path | Purpose |
|---|---|
GET /emailer/home/{key} |
Click‑through landing / open marker |
GET /emailer/goto/{key}/{url} |
Tracked outbound link redirect |
GET /emailer/logoimg/{key} |
Tracking pixel (logo image) |
GET /emailer/unsubscribe/{key} |
One‑click unsubscribe |
GET /emailer/subscribe/{key} |
Re‑subscribe |
GET /emailer/status/{key} |
Per‑message delivery status page |
GET /emailer/get/{key}/{secret} |
Read‑only rendered body (secret‑gated; opaque 404 when secret_key is unset) |
echo $emailer->dispatchRoute($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);The JSON management API (login / dashboard / SMTP test) is documented in
swagger.json.
A full dev stack (PHP‑FPM, Nginx, MariaDB, Redis) is provided:
cp .env_dist .env # edit passwords
make build
make upSee the Makefile for migrations-*, phpunit, phpstan, cs-* targets.
The CI matrix runs the whole tool‑chain on PHP 8.4 and 8.5. To reproduce locally you only need PHP with the extensions listed above plus Composer:
composer install
composer test # PHPUnit
composer test-coverage # PHPUnit + HTML/text coverage (needs Xdebug or PCOV)
composer phpstan # PHPStan level 7 (src + tests)
composer cs-check # PSR-12 strict (squizlabs/php_codesniffer)
composer cs-fix # auto-fix styleCurrent line coverage is ~70%. Tests live in tests/ and split into
pure unit tests (mocked DBAL connection) and integration tests that run against an
in‑memory SQLite database (tests/Support/IntegrationCase.php).
src/
Emailer.php Entry point / service locator
ConfigService.php Typed configuration
Mail.php Outgoing message value object
Sender.php Queues a message for a campaign
Controller/ HTTP + console + JSON API controllers
Model/ Active-record style domain models
Repository/ Doctrine DBAL data access
Cqrs/ Single-purpose command/query handlers
Transports/ SMTP transport (PHPMailer)
Migration/ Doctrine schema migration
Helper/, Exception/, locale/, view/
tests/ PHPUnit unit + integration suites
example/ Runnable usage examples
docker/ Local dev & CI images
Contributions are welcome — please read CONTRIBUTING.md first.
Distributed under the GNU General Public License v3.0 or later. See LICENSE.