simonschaufi/laravel-dkim
Add DKIM signing to outgoing Laravel mail with this simple package. Configure your domain, selector, and private key, and it automatically signs emails through Laravel’s mailer to improve authentication and deliverability.
simonschaufi/laravel-dkim adds DKIM signing to Laravel's mail pipeline so you can authenticate outbound mail without changing any Mailable or Mail:: call sites.
composer require simonschaufi/laravel-dkim (v3.6.0 widens support through Laravel 13.x; check composer show simonschaufi/laravel-dkim for the actual illuminate/* constraint before bumping).php artisan vendor:publish --tag=config (or --provider="Simonschaufi\LaravelDkim\DkimServiceProvider").config/dkim.php — typically domain, selector, private_key (PEM contents or a path to the key), optional passphrase, and an enabled flag.DKIM_DOMAIN, DKIM_SELECTOR, DKIM_PRIVATE_KEY, DKIM_PASSPHRASE, DKIM_ENABLED) so each environment can differ, and keep the private key out of the repo (secret mount / storage/ path, not committed).<selector>._domainkey.<domain> containing the public key (v=DKIM1; k=rsa; p=...), 2048-bit RSA, PEM key pair.DKIM-Signature header, then confirm Authentication-Results: ... dkim=pass at the receiving end.Where to look first: config/dkim.php for the knobs, the package's service provider for how the transport is decorated, and the README for the current config key names — those have shifted between major versions, so trust the installed version over blog posts.
First use case: you're sending transactional mail through a plain SMTP relay or a VPS that doesn't do provider-side DKIM signing, and you want Gmail/Outlook to stop marking it as spam. Drop in the package, add the DNS record, done — no application code changes.
Environment-gated global signing. Enable in production/staging, disable locally where Mailpit doesn't care:
// config/dkim.php
'enabled' => env('DKIM_ENABLED', false),
Set DKIM_ENABLED=true only in the environments whose DNS you control. This is the lowest-friction pattern and covers 90% of apps.
Sign only some mailers. If you send marketing mail through a provider with its own Easy DKIM (SES, Postmark, SendGrid) and transactional mail through your own relay, keep the package scoped to the mailer that needs it. Define separate mailers in config/mail.php and let each transport resolve independently, rather than signing everything and creating double signatures.
Multi-domain / multi-tenant apps. The signer reads config at send time, so a runtime override before dispatch is workable:
config([
'dkim.domain' => $tenant->mail_domain,
'dkim.selector' => $tenant->dkim_selector,
'dkim.private_key' => $tenant->dkimPrivateKey(),
]);
Mail::to($user)->send(new Receipt($order));
Wrap this in a small DkimContext service (or a queued-job middleware) so it's applied consistently, and make sure queued mail re-applies it inside the job — the config override does not travel with the queued mailable.
Key rotation without downtime. Create a new selector (202501 → 202506), publish the new TXT record, wait for DNS propagation, deploy the config change, confirm dkim=pass on the new selector, then delete the old record. Never swap the key under an existing selector.
Testing. Mail::fake() intercepts before the transport, so you will not see a signature — don't write assertions against it. Instead:
Symfony\Component\Mime\Email and asserting a DKIM-Signature header appears.array or log transport and parse the raw message, or run against Mailpit and read the .eml.Deploy checklist. php artisan config:cache, php artisan queue:restart (workers hold the old config/key in memory), and confirm the key file is readable by the www-data/queue user.
illuminate/* / symfony/mailer constraints and the CI matrix to be widened, with no config or API changes. Treat it as a safe minor bump; the real risk is app-side, not package-side: verify your other mail-adjacent packages (custom transports, mail loggers, open/click tracking) also support Laravel 13 / the newer Symfony Mailer before upgrading the framework.DKIM_PRIVATE_KEY or DKIM_DOMAIN and seeing no effect almost always means a stale bootstrap/cache/config.php. Re-run config:cache (or config:clear locally)..env values with literal \n frequently break openssl_pkey_get_private. Point private_key at a file path (secret mount, storage/app/dkim/private.key, permissions 600) instead of inlining the key.dkim=fail with body hash did not verify. Signing needs to be the last step before the wire.DKIM-Signature headers. Harmless when both pass, confusing when one fails. Disable the package for those mailers rather than debugging two keys.dkim=pass but the message "looks modified" or DMARC still fails, check alignment: the d= domain in the signature must align with your From: domain (or a subdomain under relaxed alignment), not just be a valid signature.queue:restart / Octane reload, otherwise you'll keep signing with the old key until the process recycles.Mail::fake() gives false confidence. Covered above, but it's the most common reason teams think DKIM is configured when it isn't — signatures only exist at the transport layer.$mailable->withSymfonyMessage(fn ($message) => ...) still works normally for non-signed headers.DKIM-Signature header, check the key path and storage permissions first.How can I help you explore Laravel packages today?