Search by

mailsenpai / symfony-mailer

mailsenpai

Symfony Mailer transport for SMTP Senpai by MailSenpai (EU SMTP relay and email API)

Package info

gitlab.com/smtp-senpai/symfony-mailer

Homepage

Issues

Documentation

Type:symfony-mailer-bridge

pkg:composer/mailsenpai/symfony-mailer

Statistics

Installs: 3

Dependents: 1

Suggesters: 0

Stars: 0

v1.0.0 2026-10-10 11:39 UTC

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_KEY is your SMTP Senpai API key (it starts with msp_);
  • USERNAME and PASSWORD are 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 becomes X-Metadata-<key>, tags become X-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 HttpTransportException with 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.