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

Google Mailer Laravel Package

symfony/google-mailer

Official Symfony Mailer bridge for sending email through Gmail and Google Workspace via the Gmail API. Configure OAuth2 credentials and use it as a Symfony Mailer transport for Google-backed delivery.

Deep Wiki
Context7

Getting Started

This is a transport bridge, not a mailer itself. It plugs Google's Gmail REST API into symfony/mailer, so you get MailerInterface / TransportInterface like any other transport. Reach for it when outbound SMTP is blocked (Cloud Run, GKE egress policies, restrictive corporate firewalls) or when you want sends attributed to a Workspace user's Gmail history.

Prerequisites (do these before touching the code):

  1. Enable the Gmail API in a Google Cloud project.
  2. Create OAuth2 credentials (Desktop or Web app client) with scope https://mail.google.com/ (full) or gmail.send (send-only — prefer this).
  3. Obtain an access token (Google OAuth Playground is fine for a first smoke test) or a refresh token for production.

Install and configure:

composer require symfony/google-mailer symfony/mailer
# .env
MAILER_DSN=gmail+api://[email protected]:YOUR_OAUTH_ACCESS_TOKEN@default
# config/packages/mailer.yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'

First send — nothing Google-specific in your code:

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class Notifier
{
    public function __construct(private MailerInterface $mailer) {}

    public function ping(): void
    {
        $this->mailer->send((new Email())
            ->from('[email protected]')
            ->to('[email protected]')
            ->subject('Hello from Gmail API')
            ->text('It works.'));
    }
}

Where to look first: Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory and GmailTransport. That is the entire package surface.

Implementation Patterns

Use MailerInterface everywhere. The bridge is invisible once configured. Your Email, TemplatedEmail, and NotificationEmail code is identical to the SMTP world, which makes swapping transports later a DSN change.

Prefer refresh tokens over baked-in access tokens. Access tokens expire in ~1 hour. The static DSN approach only works for a smoke test. For production, either:

  • Inject the token dynamically per request by building the transport yourself, or
  • Refresh the token in a decorator/service and set it on the DSN before mailer boot, or
  • Wrap your own TransportInterface around the bridge and call Google\Client::fetchAccessTokenWithRefreshToken() before send().

A decorator is the least invasive pattern:

final class RefreshingTransport implements TransportInterface
{
    public function __construct(
        private TransportInterface $inner,
        private Google\Client $client,
    ) {}

    public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
    {
        if (!$this->client->getAccessToken() || $this->client->isAccessTokenExpired()) {
            $this->client->fetchAccessTokenWithRefreshToken();
        }
        return $this->inner->send($message, $envelope);
    }

    public function __toString(): string { return 'gmail+api'; }
}

Route through Messenger for resilience. Gmail API calls can be slow and rate-limited; async sending makes user-facing requests snappy and gives you retries for free:

framework:
    messenger:
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': async

Keep From == authenticated user. Gmail's API rejects (or rewrites) From addresses that don't match the authenticated account or a verified alias. Centralize this in a from default on Email/TemplatedEmail and assert it in tests.

Testing: don't hit the API. In test env set MAILER_DSN=null://null (or in-memory://) and assert on MessageEvent via mailer.assertEmailIsQueued() / the profiler. If you specifically need to test the Google DSN path, mock the Google\Client and inject it into a transport built by hand.

Gotchas and Tips

  • The scheme is gmail+api, not gmail. The bridge does not provide SMTP-over-Gmail; gmail:// and gmail+smtp:// are not registered by this package. If you want SMTP you don't need this bridge at all — use smtp://smtp.gmail.com:587 with an app password.
  • Access tokens are short-lived. The DSN-baked token pattern will stop working after ~3600 seconds. Budget a refresh mechanism from day one; do not ship a static token to production.
  • From must be the authenticated Gmail address (or a verified alias/Send-As in Workspace). Mismatches produce a cryptic 400 from the Gmail API that looks like a serialization error if you catch it at the Mailer layer — bubble up the Google\Service\Exception on failure.
  • Daily send limits are real. Consumer Gmail caps around 500 recipients/day; Workspace around 2,000. The API returns 429/403 quota errors, not bounce messages. Treat rate-limit responses as retryable in your Messenger retry strategy.
  • Payloads are Base64url-encoded automatically. Don't pre-encode. Large attachments count toward Gmail's 25 MB message ceiling and inflate the API call; if you regularly send attachments, consider link-based delivery instead.
  • The Google client is a heavy dependency. google/apiclient pulls a large tree; if image size matters, run composer require google/apiclient:^2.0 with extra.google/apiclient-services filtered to just Gmail, or use the google/apiclient-services allowlist to trim.
  • No delivery, bounce, or DSN feedback through this transport. It's a send-only bridge; the returned SentMessage carries a Gmail message id you can log, but you will not get async bounces. Pair with a webhook consumer on Gmail push notifications if you need that.
  • Debugging 401s: confirm the token isn't expired (Google\Client::isAccessTokenExpired()), that scopes include gmail.send or https://mail.google.com/, and that the Gmail API (not just OAuth consent) is enabled on the Cloud project. Three independent places to get it wrong.
  • Extension point: if DSN-driven construction doesn't fit (e.g. you store tokens in a vault per tenant), skip the factory and construct new GmailTransport($googleClient, $dispatcher) directly, then wrap it in your own TransportInterface. The factory is convenient, not mandatory.
  • default host is conventional, not required. Any host segment is accepted; the email and token come from user/password. If you see auth failures, check your DSN parser isn't mangling tokens containing + or / — URL-encode them.
  • Version skew: this bridge tracks the Symfony minor version. Pin symfony/mailer and symfony/google-mailer to the same ^7.x constraint to avoid TransportFactory interface drift during deploys.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib