w4msolutions / w4m-email-gateway
Symfony Mailer transport for the W4M Email Gateway service, with Laravel and Yii2 adapters.
Requires
- php: ^8.4
- psr/log: ^1.1|^2.0|^3.0
- symfony/mailer: ^6.4|^7.0|^8.0
- symfony/mime: ^6.4|^7.0|^8.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^12.0
- slevomat/coding-standard: ^8.29
- w4msolutions/php-standard: ^2.0
- yiisoft/yii2: ^2.0.45
- yiisoft/yii2-symfonymailer: ^4.0
Suggests
- illuminate/mail: Required by the Laravel adapter (^12.0|^13.0).
- yiisoft/yii2-symfonymailer: Required by the Yii2 adapter (^4.0).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-08 18:50:37 UTC
README
Symfony Mailer transport that sends emails through the W4M Email Gateway API, with ready-made adapters for Laravel and Yii2.
Package
- Name:
w4msolutions/w4m-email-gateway - Type:
library - Namespace:
W4MSolutions\W4mEmailGateway - Version: 2.0.0
Supported versions
| Component | Supported versions |
|---|---|
| PHP | 8.4 and later (^8.4) |
| Laravel | 12.x, 13.x |
| Yii2 | 2.0.45 and later, with yiisoft/yii2-symfonymailer ^4.0 |
| Symfony Mailer / Mime | 6.4, 7.x, 8.x |
The package has no hard dependency on either framework. Laravel and Yii2 are
only needed by the adapter you use. Symfony 8 is only reachable from Laravel
13: yiisoft/yii2-symfonymailer 4 supports Symfony 6.4 and 7.
The test suite runs against Laravel 12 and 13, Yii2 2.0.55 and Symfony 6.4, 7.4 and 8.1.
Features
- One delivery implementation shared by every framework:
Transport\GatewayTransport, aSymfony\Component\Mailer\Transport\TransportInterface. - Sends payload to the gateway
/sendendpoint with API key authentication. - Converts attachments to the gateway
attachments[]payload (base64 + filetype). - Rewrites inline CID embeds (for example
cid:image.jpg) intodata:URIs. - Retries failed sends with incremental cooldown (default: 3 attempts, waits 1s then 3s).
- Disable flag that short-circuits delivery without an HTTP request.
- Messages without a From address are sent with a default From (see Sender address).
- Pluggable HTTP client (
Client\GatewayClientInterface) and PSR-3 logging.
Installation
composer require w4msolutions/w4m-email-gateway
Laravel
The service provider is discovered automatically. If you disabled package discovery, register it manually:
// bootstrap/providers.php 'providers' => [ \W4MSolutions\W4mEmailGateway\Laravel\EmailGatewayServiceProvider::class, ],
Optionally publish the config:
php artisan vendor:publish --provider="W4MSolutions\W4mEmailGateway\Laravel\EmailGatewayServiceProvider" --tag=config
Add a gateway mailer to config/mail.php:
'mailers' => [ 'gateway' => [ 'transport' => 'gateway', 'gateway_url' => env('EMAIL_GATEWAY_URL'), 'key' => env('EMAIL_GATEWAY_KEY'), 'send_path' => env('EMAIL_GATEWAY_SEND_PATH', '/send'), 'auth_header' => env('EMAIL_GATEWAY_AUTH_HEADER', 'Authorization'), 'auth_prefix' => env('EMAIL_GATEWAY_AUTH_PREFIX', ''), 'timeout' => (int) env('EMAIL_GATEWAY_TIMEOUT', 15), 'retries' => (int) env('EMAIL_GATEWAY_RETRIES', 3), 'retry_initial_delay' => (int) env('EMAIL_GATEWAY_RETRY_INITIAL_DELAY', 1), 'retry_delay_increment' => (int) env('EMAIL_GATEWAY_RETRY_DELAY_INCREMENT', 2), 'enabled' => ! filter_var(env('EMAIL_DISABLE', false), FILTER_VALIDATE_BOOLEAN), ], ],
Values missing from the mailer entry fall back to config/email-gateway.php.
Note the key is gateway_url, not url: Laravel parses a mailer url key as
an SMTP DSN.
Set MAIL_MAILER=gateway to make it the default, or use it explicitly:
Mail::mailer('gateway')->to($user->email)->send(new InvitationMail($user));
Failures throw Transport\GatewayTransportException after the last attempt,
as any Laravel mail transport does. Queued mailables and Laravel's failover
transport work as usual: a gateway failure moves a failover mailer on to the
next mailer in its list.
Per-send overrides
Build a mailer with runtime configuration when a message must use a different gateway key:
$config = array_merge(config('mail.mailers.gateway'), [ 'key' => $client->email_gateway_key, ]); Mail::build($config)->to($user->email)->send(new InvitationMail($user));
Mailers created with Mail::build() are not intercepted by Mail::fake().
The Laravel adapter sends through the Laravel HTTP client, so use
Http::fake() and Http::assertSent() to assert the gateway request in tests.
Environment variables
| Variable | Default | Description |
|---|---|---|
EMAIL_GATEWAY_URL |
(empty) | Gateway base URL |
EMAIL_GATEWAY_KEY |
(empty) | Gateway API key |
EMAIL_GATEWAY_SEND_PATH |
/send |
Send endpoint path |
EMAIL_GATEWAY_AUTH_HEADER |
Authorization |
Auth header name |
EMAIL_GATEWAY_AUTH_PREFIX |
(empty) | Auth header prefix, e.g. Bearer |
EMAIL_GATEWAY_TIMEOUT |
15 |
Request timeout in seconds |
EMAIL_GATEWAY_RETRIES |
3 |
Total send attempts |
EMAIL_GATEWAY_RETRY_INITIAL_DELAY |
1 |
Delay before first retry (s) |
EMAIL_GATEWAY_RETRY_DELAY_INCREMENT |
2 |
Extra delay per retry (s) |
EMAIL_DISABLE |
false |
When true, delivery is a no-op |
MAIL_FROM_ADDRESS |
noreply@host.local |
Default From for messages without one |
MAIL_FROM_NAME |
(empty) | Default From name for messages without one |
Yii2
Requires yiisoft/yii2-symfonymailer (^4.0) in the application.
'components' => [ 'mailer' => [ 'class' => \W4MSolutions\W4mEmailGateway\Yii2\Mailer::class, 'viewPath' => '@common/mail', 'gatewayUrl' => 'https://mail-gateway.local', 'gatewayApiKey' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'authHeaderName' => 'Authorization', // Optional, default: 'Authorization' 'authHeaderPrefix' => '', // Optional, default: '' 'sendPath' => '/send', // Optional, default: '/send' 'requestTimeout' => 15, // Optional, default: 15 seconds 'maxRetries' => 3, // Optional, default: 3 'retryInitialDelaySeconds' => 1, // Optional, default: 1 'retryDelayIncrementSeconds' => 2, // Optional, default: 2 'enabled' => true, // Optional, default: true; false skips the HTTP request 'useFileTransport' => false, // Yii option; true saves .eml files and skips the gateway ], ],
The component extends yii\symfonymailer\Mailer, so compose, render, layouts
and every yii\symfonymailer\Message call keep working. Do not set the
transport option: the component builds the gateway transport itself.
send() returns false and logs through Yii::error() on any failure,
following the yii\mail\MailerInterface contract; it never throws. Retry warnings go to the Yii log under the
W4MSolutions\W4mEmailGateway\Yii2\Mailer category. Set the logger
property to any PSR-3 logger to route them elsewhere.
Other frameworks
Anything that uses Symfony Mailer can use the transport directly:
use Symfony\Component\Mailer\Mailer; use W4MSolutions\W4mEmailGateway\Transport\GatewayTransport; $mailer = new Mailer(new GatewayTransport([ 'gateway_url' => 'https://mail-gateway.local', 'key' => 'xxxxxxxx', ]));
The constructor also accepts a Client\GatewayClientInterface, a
Payload\GatewayPayloadBuilder and a Psr\Log\LoggerInterface. All config
keys and their defaults are listed in GatewayTransport::DEFAULTS.
Sender address
A From set on the message is always sent as from and from_name.
A message without a From is sent with a default From instead:
- Laravel:
MAIL_FROM_ADDRESSandMAIL_FROM_NAME, falling back tonoreply@host.local. Override withdefault_fromanddefault_from_nameinconfig/email-gateway.phpor in the mailer entry. Laravel normally setsmail.fromon every message already, so this only applies when that is empty. - Yii2:
noreply@host.local. Set a From for every message through the component'smessageConfig('messageConfig' => ['from' => [...]]). - Other frameworks: the
default_fromanddefault_from_namekeys of theGatewayTransportconfig array.
What the gateway does with it depends on how it delivers:
- Through Microsoft 365 the gateway always sends as its mailbox and ignores the From.
- Through regular SMTP the gateway sends with that From, or returns an error when the SMTP server does not allow the address.
Retry behavior
Default retry flow is:
- Attempt 1 immediately.
- If it fails, wait 1 second.
- Attempt 2.
- If it fails, wait 3 seconds (1 + 2).
- Attempt 3.
Tune with retries, retry_initial_delay and retry_delay_increment
(Laravel) or maxRetries, retryInitialDelaySeconds and
retryDelayIncrementSeconds (Yii2).
Class reference
Transport\GatewayTransport: Symfony transport; validates config, maps the payload, runs the retry loop and throwsGatewayTransportExceptionafter the final failure.Transport\GatewayTransportException: transport failure or configuration error.Payload\GatewayPayloadBuilder: maps aSymfony\Component\Mime\Emailinto the gateway JSON payload.Payload\CidEmbedRewriter: replacescid:references withdata:URIs.Client\GatewayClientInterface: HTTP POST abstraction.Client\GatewayClient: default client using cURL with a stream fallback.Laravel\EmailGatewayServiceProvider: registers thegatewaymail transport.Laravel\HttpClientGatewayClient: client built on the Laravel HTTP client.Yii2\Mailer: Yii2 mailer component.Yii2\YiiLogger: PSR-3 logger forwarding to the Yii log.
Running tests (Docker)
Every service installs dependencies on first run and runs as your host user
(UID/GID, default 1000).
| Command | Runs |
|---|---|
docker compose run --rm tests |
All suites |
docker compose run --rm tests-unit |
Framework-neutral transport, payload and HTTP client |
docker compose run --rm tests-laravel |
Laravel adapter in a Testbench application |
docker compose run --rm tests-yii2 |
Yii2 adapter, including a Yii application with views |
docker compose run --rm lint |
PHP_CodeSniffer with the W4M standard |
Run a single file:
docker compose run --rm tests sh docker/run-tests.sh tests/Unit/GatewayTransportTest.php
Notes
- The gateway schema documents
to,cc,bcc,subject,body,attachments,from_nameandreply_to. The transport also forwards afromfield when the message has one. - Invalid messages (for example without recipients) fail before any request is sent, so a failed send never delivers.