tereta/email

Maintainers

Package info

gitlab.com/tereta/framework/email

Issues

pkg:composer/tereta/email

Statistics

Installs: 222

Dependents: 3

Suggesters: 0

Stars: 0

1.0.11 2026-05-10 13:53 UTC

This package is auto-updated.

Last update: 2026-05-18 20:01:05 UTC


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 nameClassPurpose
mail (default)Tereta\Email\Strategies\MailPHP mail() via the local MTA (sendmail/msmtp).
smtpTereta\Email\Strategies\SmtpDirect SMTP connection. SSL and STARTTLS support, AUTH LOGIN.
logTereta\Email\Strategies\LogSerialises 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 to address (supports the Name <email> form) β€” otherwise raises InvalidArgumentException.
  • Strips \r, \n, \0 from every header and the subject, blocking header injection (e.g. attempts to inject Bcc: 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') with to, subject, and the list of header keys.
  • error β€” transport failures (with to, subject, and the exception message). The exception is rethrown after logging β€” wrap calls in try/catch if 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