Tereta/Email Module
Overview
Email delivery via pluggable transport strategies. Ships three built-in strategies (mail, smtp, log), a factory for registering and selecting them, and a validator service that protects against header injection.
The module integrates with tereta/config (per-store SMTP settings) and tereta/queue (asynchronous delivery via tereta/queue-email), but works standalone.
Requirements
- PHP 8.4+
tereta/core,tereta/di
Transports
| Registry name | Class | Purpose |
|---|---|---|
mail (default) | Tereta\Email\Strategies\Mail | PHP mail() via the local MTA (sendmail/msmtp). |
smtp | Tereta\Email\Strategies\Smtp | Direct SMTP connection. SSL and STARTTLS support, AUTH LOGIN. |
log | Tereta\Email\Strategies\Log | Serialises an RFC822 message to <root>/var/emails/*.eml or stores it in memory (for tests). |
Tereta\Email\Interfaces\Sender:
public function send(
string $to,
string $subject,
string $body,
array $headers = [],
array $meta = []
): void;
Configuration
From code
use Tereta\Core\Data\Value;
use Tereta\Email\Factories\Sender as EmailFactory;
# default transport
EmailFactory::singleton()->setDefault('smtp');
# SMTP parameters
EmailFactory::singleton()->configure('smtp', Value::factory()->create()
->set('host', 'smtp.gmail.com')
->set('port', 587)
->set('username', '[email protected]')
->set('password', 'app-password')
->set('encryption', 'tls') // 'tls' | 'ssl' | ''
->set('timeout', 30)
);
configure() accepts a Value or array<string, mixed> and is merged on top of the transport defaults (for smtp: gmail:587 + TLS, no credentials).
Via tereta/config (per-store)
If tereta/config is in use, the module auto-applies SMTP settings on the route.requestModel.created event (see src/Events/Configure.php). Expected pool keys:
mail.smtp.host
mail.smtp.port
mail.smtp.username
mail.smtp.password
mail.smtp.encryption
mail.smtp.timeout
Values are resolved for the active store ID and pushed into the factory through EmailFactory::configure('smtp', …).
Usage
Sending a single email
use Tereta\Email\Factories\Sender as EmailFactory;
EmailFactory::singleton()->create()->send(
to: '[email protected]',
subject: 'Subject',
body: '<p>Email body</p>',
headers: ['From' => '[email protected]', 'Reply-To' => '[email protected]']
);
create() with no argument returns the default transport. To pick a specific one — create('smtp'). Extra constructor arguments can also be supplied:
EmailFactory::singleton()->create('smtp', ['host' => 'smtp.mailgun.org', 'port' => 587]);
They are merged on top of the factory's stored configuration.
Header safety
Tereta\Email\Services\Validator automatically:
- Validates the
toaddress (supports theName <email>form) — otherwise raisesInvalidArgumentException. - Strips
\r,\n,\0from every header and the subject, blocking header injection (e.g. attempts to injectBcc:via a tampered subject).
Validator::singleton()->validateEmail() / sanitizeHeaderValue().
log strategy for tests
use Tereta\Email\Factories\Sender as EmailFactory;
use Tereta\Email\Strategies\Log\Memory as EmailMemory;
EmailFactory::singleton()->setDefault('log');
# Writes to /var/emails/<date>_<recipient>_<rand>.eml:
EmailFactory::singleton()->create('log')->send('[email protected]', 'subj', 'body');
# In-memory storage, no filesystem touched:
EmailFactory::singleton()->configure('log', ['memorize' => true]);
EmailFactory::singleton()->create('log')->send('[email protected]', 'subj', 'body');
$captured = EmailMemory::singleton()->all(); // or ->single()
Registering a custom strategy
use Tereta\Email\Factories\Sender as EmailFactory;
use Tereta\Email\Interfaces\Sender;
class Mailgun implements Sender
{
public function __construct(
private readonly string $domain,
private readonly string $apiKey,
) {}
public function send(string $to, string $subject, string $body, array $headers = [], array $meta = []): void
{
// HTTP call to the Mailgun API
}
}
EmailFactory::singleton()
->register('mailgun', Mailgun::class)
->configure('mailgun', ['domain' => 'mg.example.com', 'apiKey' => '...'])
->setDefault('mailgun');
register() verifies the class implements Sender and throws InvalidArgumentException otherwise. Constructor arguments are resolved through tereta/di, with the missing ones filled in from configure().
Logging
The module registers an email channel (<ROOT_DIRECTORY>/var/logs/email.log). It writes:
debug— send events ('Mail send' / 'SMTP send' / 'Log send') withto,subject, and the list of header keys.error— transport failures (withto,subject, and the exception message). The exception is rethrown after logging — wrap calls intry/catchif you need fallback or retry behaviour.
tereta/logger:
use Tereta\Logger\Services\Channel\Config as LogChannelConfig;
LogChannelConfig::set('debug', true);
LogChannelConfig::set('error', true);
Asynchronous delivery
For queueing, install tereta/queue-email. It registers a queue strategy that publishes the payload to Kafka; on the consumer side the message hands itself back to the default sender for actual delivery. See the tereta/queue-email package documentation.
Author and License
Author: Tereta Alexander
Website: tereta.dev
License: Apache License 2.0. See LICENSE.
www.████████╗███████╗██████╗ ███████╗████████╗ █████╗
╚══██╔══╝██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗
██║ █████╗ ██████╔╝█████╗ ██║ ███████║
██║ ██╔══╝ ██╔══██╗██╔══╝ ██║ ██╔══██║
██║ ███████╗██║ ██║███████╗ ██║ ██║ ██║
╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝
.dev
Copyright (c) 2008-2026 Tereta Alexander