Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Swiftmailer Send Grid Bundle Laravel Package

gulaandrij/swiftmailer-send-grid-bundle

A Symfony bundle that integrates SendGrid with Swift Mailer by providing a dedicated transport, letting applications send transactional email through SendGrid's API with minimal configuration and familiar Swift Mailer usage.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you install anything. This bundle is a 2018-era Symfony bridge for Swiftmailer's SendGrid transport. Swiftmailer reached EOL in November 2021 and the Symfony SwiftmailerBundle was dropped in Symfony 6, so this is only viable on Symfony 2.x–5.x with swiftmailer/swiftmailer 6.x and PHP 7.x. Before adding it, check whether the swiftmailer config in your swiftmailer-bundle version already accepts transport: sendgrid with an api_key — if it does, this bundle is redundant.

Minimal steps

  1. Confirm your constraints in composer.json: PHP version, symfony/framework-bundle, swiftmailer/swiftmailer, and sendgrid/sendgrid. The SendGrid PHP SDK had two incompatible generations (v5 SendGrid\Mail / v6+ SendGrid\Mail\Mail, SendGrid\Mail\To, etc.), and the bundle was written against one of them — pin whichever version it expects.
  2. Install:
    composer require gulaandrij/swiftmailer-send-grid-bundle sendgrid/sendgrid
    
  3. Register the bundle (namespace per its README — typically Gulaandrij\SwiftmailerSendGridBundle\SwiftmailerSendGridBundle):
    // app/AppKernel.php (Symfony <4) or config/bundles.php
    new Gulaandrij\SwiftmailerSendGridBundle\SwiftmailerSendGridBundle(),
    
  4. Configure the API key from env, not from a committed file:
    # config/packages/swiftmailer_send_grid.yaml
    swiftmailer_send_grid:
        api_key: '%env(SENDGRID_API_KEY)%'
    
  5. Point the swiftmailer mailer at the bundle's transport service (service id is defined in Resources/config/; verify it, don't guess), or leave swiftmailer configured as usual and inject the transport into your own Swift_Mailer.

Where to look first

  • DependencyInjection/Configuration.php and …Extension.php — the only source of truth for real config keys and defaults.
  • Resources/config/services.xml (or .yml) — the transport's service id and whether it is wired as swiftmailer.mailer.default.transport.
  • The transport class itself — this is where Swift_Mime_Message is translated into a SendGrid Mail object. Everything this package can and cannot do lives in that one file: to/cc/bcc, attachments, inline images, custom headers, reply-to, sandbox mode, templates.
  • The test suite, if present — fastest way to see the expected SDK version and payload shape.

First use case: send one transactional email through Swift_Mailer exactly as you would with SMTP. No application code should change — $mailer->send($message) — which is the whole point of a transport. Get a password-reset email to yourself, then check the SendGrid Activity Feed to confirm it left the building.

Implementation Patterns

Standard transactional send. Build the Swift_Message, set both HTML and plain-text parts (addPart()), and hand it to the mailer. Because the bundle is a transport, it composes cleanly with everything you already do: Twig-rendered bodies, Swift_Message::setEncoder(), address objects, Swift_Plugins_* decorators.

$message = (new Swift_Message('Reset your password'))
    ->setFrom('[email protected]', 'Example')
    ->setTo($user->getEmail())
    ->setBody($html, 'text/html')
    ->addPart($text, 'text/plain');

$mailer->send($message);

Two-mailer topology is the most useful pattern here. Keep SendGrid for transactional traffic and a real SMTP/DSN-capable transport for everything else, so a SendGrid 401 or outage doesn't stop password resets and so you don't lose bounce data on bulk mail. Register the bundle's transport as a service and build a dedicated mailer:

services:
    app.mailer.transactional:
        class: Swift_Mailer
        arguments: ['@<bundle transport service id>']

Then alias it as Swift_Mailer $transactionalMailer for autowiring, or expose it under a named swiftmailer.mailers key.

Always spool when sending more than a handful. Each send is a synchronous HTTPS round-trip; 500 recipients in a request loop will time out your worker. Use swiftmailer.spool (file or a custom Swift_Spool) and drain with a cron/worker:

bin/console swiftmailer:spool:send --message-limit=200 --time-limit=60

Run that under Supervisor, and wrap it so a single transport exception doesn't abandon the rest of the spool.

Rate-limit and back off. SendGrid's v3 mail-send limit is ~100 requests/second per account and it will return 429. Since each send() is one request, throttle in your batch job (sleep between chunks) and treat Swift_TransportException as retryable only for 429/5xx — never blanket-retry a 400, which will just fail again and burn quota.

Observe delivery separately from sending. send() returning true only means SendGrid accepted the request (HTTP 202). The pattern that actually works: configure the SendGrid Event Webhook to a Symfony controller that verifies the signature, enqueues the event, and updates your message/recipient status table on delivered, bounce, dropped, spamreport, unsubscribe. Without this you will have no idea that half your list is suppressed.

Prefer SendGrid-side templates when you can. If the transport doesn't surface template_id / dynamic_template_data, don't fight it — either render in Twig (simplest, keeps versioning in your repo) or subclass the bundle's transport and override the message-conversion method to attach the template. That single method is the intended extension point for anything the bundle doesn't expose: templates, mail_settings.sandbox_mode, tracking_settings, categories, custom_args.

Test with a null transport, never a live key. Set swiftmailer.disable_delivery: true (or point the mailer at Swift_NullTransport / a Swift_Transport_SpoolTransport with an array spool) in test env, and assert on the spooled Swift_Message objects. In dev, a Swift_Events_SendListener that logs the recipient, subject, and message-id is worth more than any debug toolbar panel.

Guard the key. '%env(SENDGRID_API_KEY)%', never committed. The key needs only the Mail Send permission — do not reuse a full-access key. Restrict outbound senders with a domain authentication entry in SendGrid so a leaked key can't spoof your domain.

Migration target. For new work, symfony/mailer + symfony/sendgrid-mailer covers the same ground with maintained code and native Symfony\Component\Mime\Email, plus MailerInterface and TransportInterface that are far easier to decorate. Porting is mostly mechanical: Swift_Message → Email, $mailer->send() → $mailer->send().

Gotchas and Tips

  • SDK version mismatch is the #1 install failure. The bundle predates sendgrid/sendgrid v7's SendGrid\Mail\* namespace. If you get Class 'SendGrid\Mail' not found (or the inverse), you installed the wrong major — pin sendgrid/sendgrid to whatever the bundle's composer.json was built against, and lock it.
  • Nothing SMTP-related applies. host, port, encryption, username, password, auth_mode, and stream_options from swiftmailer config are silently ignored. If mail "sends" but never arrives, check that the mailer is actually bound to this transport and not to a FailoverTransport/SpoolTransport chain that swallowed the call — walk $mailer->getTransport() and unwrap.
  • send() can throw after partial acceptance. With multiple recipients, a transport exception may surface after some were accepted; a naive retry duplicates mail. Use $mailer->send($message, $failedRecipients) and log $failedRecipients, and make retries idempotent via a X-Message-Id-style custom arg that SendGrid echoes back through the event webhook.
  • Suppressions look like success. SendGrid silently drops mail to bounced/unsubscribed/spam-reported addresses. If a user swears they never got the email and the Activity Feed says "Dropped," that's why. Only the event webhook (or manual suppression-list checks) reveals this.
  • 403 Forbidden usually isn't your code. It means the From domain isn't authenticated or the single sender isn't verified in SendGrid. Fix it in the SendGrid dashboard, not in Symfony.
  • Payload size. The whole message — including base64 attachments — goes into one JSON POST with SendGrid's ~30 MB limit; base64 inflates by ~33% and PHP holds it all in memory. Don't attach large PDFs; send a signed link instead.
  • No curl timeout by default. The underlying SDK call can hang a PHP-FPM worker indefinitely on a slow SendGrid response. Set default_socket_timeout, ensure curl's connect timeout is configured (often via the SDK, sometimes only by forking the transport), and keep sends out of user-facing requests via the spool.
  • Spool drain aborts on exception. swiftmailer:spool:send stops when the transport throws; a poisoned message can wedge the queue. Run it with --message-limit, monitor exit codes, and consider a dead-letter path.
  • setReturnPath() / envelope sender behaves differently over an API transport than over SMTP; SendGrid derives bounce routing from from and your domain settings. Set Reply-To explicitly and add a List-Unsubscribe header for anything bulk-ish — SendGrid expects it and mailbox providers will penalise you without it.
  • Sandbox mode is your friend during integration. If the transport doesn't expose mail_settings.sandbox_mode, subclass it and enable sandbox while you're validating payloads — you get 202s and full validation without delivering mail. It's the cleanest way to test the whole path in staging.
  • Debug by dumping the payload. Decorate the transport service (same id, decorates: in Symfony 4.1+, or an explicit wrapper) and log the SendGrid\Mail\Mail JSON before it's POSTed. That one log line answers 90% of "why did this render wrong" questions, and it's the only way to see header/cid/attachment mapping mistakes.
  • Config cache and env processors. Changing SENDGRID_API_KEY requires cache:clear in prod (config:cache doesn't exist pre-4.2 but %env()% is resolved at container compile time in some setups). If the key contains characters that trip the YAML/DIC parser, use a ! prefix or an env processor rather than quoting gymnastics.
  • It is unmaintained; treat it as a liability. Nine stars, zero dependents, no release since July 2018, and both of its upstream dependencies are EOL. Don't build new features on it: vendor/fork the transport class into your own App\Mailer\Transport (it's small), pin it in composer.json with a comment explaining why, and schedule the symfony/mailer migration. If you must keep it, add a test that asserts the generated SendGrid payload shape so an accidental SDK bump fails CI instead of production.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource