mailsenpai / symfony-mailer
Symfony Mailer transport for SMTP Senpai by MailSenpai (EU SMTP relay and email API)
Package info
gitlab.com/smtp-senpai/symfony-mailer
Type:symfony-mailer-bridge
pkg:composer/mailsenpai/symfony-mailer
Requires
- php: >=8.2
- symfony/mailer: ^7.2|^8.0
Requires (Dev)
- phpunit/phpunit: ^11.5|^12.0
- symfony/http-client: ^7.2|^8.0
- symfony/http-foundation: ^7.2|^8.0
- symfony/webhook: ^7.2|^8.0
Suggests
- symfony/http-client: Required by the API transport (smtpsenpai+api://)
- symfony/webhook: To receive SMTP Senpai webhook events as Symfony RemoteEvents
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-25 09:23:24 UTC
README
Symfony Mailer transport for SMTP Senpai by MailSenpai, the EU-hosted SMTP relay and transactional email API.
It provides an API transport (HTTPS), an SMTP transport, a TransportFactory for DSNs and a webhook
request parser that turns SMTP Senpai events into Symfony RemoteEvents.
Docs: https://en.mailsenpai.com/smtp-api/ · Product: https://en.mailsenpai.com/smtp-senpai/ · All integrations: https://en.mailsenpai.com/integrations/ Source code and issues: https://gitlab.com/smtp-senpai/symfony-mailer
Installation
composer require mailsenpai/symfony-mailer symfony/http-client
Requires PHP 8.2+ and Symfony Mailer 7.2+ (8.x supported).
Register the factory (third-party bridges are not auto-registered by FrameworkBundle):
# config/services.yaml
services:
MailSenpai\SymfonyMailer\Transport\MailSenpaiTransportFactory:
parent: mailer.transport_factory.abstract
tags: ['mailer.transport_factory']
Configuration
# API (recommended: HTTPS, works where outgoing SMTP ports are blocked)
MAILER_DSN=smtpsenpai+api://API_KEY@default
# SMTP (relay.mailsenpai.com, port 2525 or 587, STARTTLS required)
MAILER_DSN=smtpsenpai+smtp://USERNAME:PASSWORD@default
where:
API_KEYis your SMTP Senpai API key (it starts withmsp_);USERNAMEandPASSWORDare your SMTP credentials (URL-encode them: the username contains@, write it as%40).
You find both in the MailSenpai customer area, SMTP Senpai page. smtpsenpai:// is an alias of smtpsenpai+smtp://.
Without a framework:
use MailSenpai\SymfonyMailer\Transport\MailSenpaiTransportFactory;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
$transport = (new Transport([new MailSenpaiTransportFactory()]))->fromString('smtpsenpai+api://msp_xxx@default');
$mailer = new Mailer($transport);
// or directly: new MailSenpai\SymfonyMailer\Transport\MailSenpaiApiTransport('msp_xxx')
Features (API transport)
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
$email = (new Email())
->from(new Address('orders@yourdomain.com', 'Your shop')) // verified domain
->to('customer@example.com')
->replyTo('support@yourdomain.com')
->subject('Order 10293 confirmed')
->text('Thanks, your order is confirmed.')
->html('<p>Thanks, your order is confirmed.</p>')
->attachFromPath('/path/to/invoice.pdf');
$email->getHeaders()
->addTextHeader('X-Order', '10293') // custom X-... headers are kept
->addTextHeader('Idempotency-Key', 'order-10293') // sent as HTTP header, never twice within 24h
->add(new TagHeader('orders')); // becomes X-Tag: orders
$mailer->send($email);
// To read the Message-ID given by SMTP Senpai, send through the transport (it returns a SentMessage):
$sent = $transport->send($email);
$sent->getMessageId();
Good to know:
- every recipient (To, Cc and Bcc) receives an individual copy, so Cc addresses are not visible to the other recipients; use the SMTP transport if you need a visible Cc;
- up to 50 recipients and 10 attachments (10 MB in total) per email; inline images are sent as regular attachments;
- only
X-...headers are kept; Symfony metadata becomesX-Metadata-<key>, tags becomeX-Tag; - recipients on your suppression list are skipped and logged as a warning, they do not make the send fail;
- API errors (unverified domain, volume used up, ...) raise
HttpTransportExceptionwith the API message (in Italian).
Webhook
Set the webhook from the customer area or the API, keep the whsec_... secret, then:
# config/services.yaml
services:
MailSenpai\SymfonyMailer\RemoteEvent\MailSenpaiPayloadConverter: ~
mailer.webhook.request_parser.smtpsenpai:
class: MailSenpai\SymfonyMailer\Webhook\MailSenpaiRequestParser
arguments: ['@MailSenpai\SymfonyMailer\RemoteEvent\MailSenpaiPayloadConverter']
# config/packages/webhook.yaml
framework:
webhook:
routing:
smtpsenpai:
service: mailer.webhook.request_parser.smtpsenpai
secret: '%env(MAILSENPAI_WEBHOOK_SECRET)%'
The webhook URL is then https://yoursite.example/webhook/smtpsenpai. Consume the events:
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface;
use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;
use Symfony\Component\RemoteEvent\RemoteEvent;
#[AsRemoteEventConsumer('smtpsenpai')]
final class MailSenpaiEventConsumer implements ConsumerInterface
{
public function consume(RemoteEvent $event): void
{
if ($event instanceof MailerDeliveryEvent && MailerDeliveryEvent::BOUNCE === $event->getName()) {
// $event->getRecipientEmail(), $event->getReason(), $event->getId() (Message-ID)
}
}
}
The parser checks X-MailSenpai-Signature (t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>, 5 minutes tolerance).
Event mapping: sent → received, delivered, bounce, defer → deferred, dropped, complaint → spam, open, click.
Tests
composer install
vendor/bin/phpunit
License
MIT, © MailSenpai.
First received message and troubleshooting
On staging, choose either Mailer::send or TransportInterface::send for one controlled message; do not call both just to inspect the same send. If you need a SentMessage and Message-ID, use the transport return value as shown above. Then test your real application notification.
A relay/API acceptance result is distinct from inbox placement. Check the actual received message and its authentication headers. Keep API keys and customer data out of public screenshots and logs.
If authentication fails, check the key and account state. For a sender-domain rejection, verify the sender domain. For quota/rate errors, respect the response and the plan limit rather than retrying blindly. For a timeout, check outbound HTTPS access. Use a stable idempotency key for retries where the integration exposes it.
Package catalogue · API errors and delivery events · Integration guides
Service logs are retained for 7 days on the free plan and 90 days on paid plans. Local application logs may have different retention.