tereta / email
Requires
- php: >=8.4
- tereta/core: ^1.0
- tereta/di: ^1.0
README
π Π ΡΡΡΠΊΠΈΠΉ | English
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). |
All strategies implement 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', 'user@gmail.com')
->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: 'recipient@example.com',
subject: 'Subject',
body: '<p>Email body</p>',
headers: ['From' => 'noreply@yourdomain.com', 'Reply-To' => 'support@yourdomain.com']
);
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).
When you implement new strategies, always go through 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('to@example.com', 'subj', 'body');
# In-memory storage, no filesystem touched:
EmailFactory::singleton()->configure('log', ['memorize' => true]);
EmailFactory::singleton()->create('log')->send('to@example.com', '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.
Enable the relevant levels in 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