This plugin adds a DKIM signature to outgoing emails using Symfony Mime.
Assume you are in the root of your Winter CMS installation.
Just run this command
composer require hounddd/wn-maildkim-pluginClone this repo into your winter plugins folder.
cd plugins
mkdir hounddd && cd hounddd
git clone https://github.com/Hounddd/wn-maildkim-plugin maildkimSome domain or email providers generate DKIM keys automatically.
- Contact their support or documentation to obtain:
- the private key
- the DKIM selector they created for your domain
- If the private key cannot be retrieved, generate your own key pair locally (next section), then add a new DKIM selector manually in your DNS zone.
Use OpenSSL to generate an RSA key pair (2048 bits recommended):
# Create the local folder that will store the private and public DKIM keys.
mkdir -p storage/app/uploads/protected/dkim
# Generate the private RSA key used to sign outgoing emails.
openssl genrsa -out storage/app/uploads/protected/dkim/private.key 2048
# Derive the public key that must be published in your DNS TXT record.
openssl rsa -in storage/app/uploads/protected/dkim/private.key -pubout -out storage/app/uploads/protected/dkim/public.pemPowerShell alternative (Windows):
# Create the local folder that will store the private and public DKIM keys.
New-Item -ItemType Directory -Force -Path "storage/app/uploads/protected/dkim" | Out-Null
# Generate the private RSA key used to sign outgoing emails.
openssl genrsa -out storage/app/uploads/protected/dkim/private.key 2048
# Derive the public key that must be published in your DNS TXT record.
openssl rsa -in storage/app/uploads/protected/dkim/private.key -pubout -out storage/app/uploads/protected/dkim/public.pem⚠ It is not recommended to generate DKIM keys on an online service. Generate the key pair locally, and if you use a web-based tool at all, limit it to inspecting or formatting the public key after the private key has already been created on your machine.
Extract the DNS-safe public key value (single line, without headers):
awk 'NR>1 && !/-----/ { printf "%s", $0 } END { print "" }' storage/app/uploads/protected/dkim/public.pemPowerShell alternative (Windows):
(Get-Content "storage/app/uploads/protected/dkim/public.pem" | Where-Object { $_ -notmatch "-----" }) -join ""This output is the p= value used in the DKIM TXT record, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- lines.
If you want to do it manually, copy only the base64 text inside the public key file and join it into a single line.
Create a TXT DNS record on your domain:
-
Choose a selector, for example
dkim. -
Use this host/name:
dkim._domainkey.your-domain.tld -
Use this TXT value:
v=DKIM1; k=rsa; p=<public key without spaces or line breaks> -
Wait for DNS propagation, then verify the record:
nslookup -type=TXT dkim._domainkey.your-domain.tld
PowerShell alternative (Windows):
Resolve-DnsName -Name dkim._domainkey.your-domain.tld -Type TXT
Reference sites for DKIM checks:
- MXToolbox DKIM Lookup (quick DNS TXT lookup)
- dmarcian DKIM Inspector (DKIM record syntax and value checks)
- Mailhardener DKIM Record Check (additional DNS validation)
For an end-to-end test of a signed message (not only DNS), use mail-tester.
Keep the private key in a non-public file path, for example:
storage/app/uploads/protected/dkim/private.key
Then configure the plugin values to match your DNS setup:
DKIM_PRIVATE_KEY=storage/app/uploads/protected/dkim/private.keyDKIM_DOMAIN=your-domain.tldDKIM_SELECTOR=dkim
DKIM_SELECTOR must exactly match the selector used in your DNS host (for example: dkim._domainkey.your-domain.tld means DKIM_SELECTOR=dkim).
Configuration is defined in config/config.php and can be provided through environment variables.
Example .env:
DKIM_ENABLED=true
DKIM_PRIVATE_KEY=storage/app/uploads/protected/dkim/private.key
DKIM_DOMAIN=example.com
DKIM_SELECTOR=dkim
DKIM_PASSPHRASE=
DKIM_ALGORITHM=rsa-sha256
DKIM_LOG_SUCCESS=false
Supported algorithms:
- rsa-sha256 (default)
- ed25519-sha256
If DKIM_DOMAIN is not set, the plugin falls back to the host from app.url. That fallback uses the host as-is.
If your APP_URL is https://www.example.net/, the fallback domain will be www.example.net.
If your DKIM record is published on the bare domain, set DKIM_DOMAIN=example.net explicitly.
DKIM_LOG_SUCCESS can be set to true temporarily for diagnostics to log successful DKIM signature application.
- The plugin listens to
mailer.prepareSendevent and signs at real send time. - This includes queued emails processed by workers.
- If configuration is incomplete or signing fails, emails are still sent unsigned.
- Failures are logged for visibility.
The plugin also provides a Winter CMS settings page that displays:
- configuration issues
- sample signature result (without sending a real email)
- DKIM DNS lookup result
- generated DNS TXT value and derived public key
Open the backend settings page for Mail DKIM to review these diagnostics in one place.
All commands return exit code 0 on success and 1 on failure.
Verifies that the current configuration can sign a sample email without sending real mail:
php artisan maildkim:signature-check
Checks DKIM DNS TXT record for configured selector/domain:
php artisan maildkim:dns-check
Optional overrides:
php artisan maildkim:dns-check --selector=dkim --domain=example.com
This command also compares the DNS p= key with the public key derived from your configured private key (when readable).
Builds the DKIM public key record to publish from the configured private key:
php artisan maildkim:dns-record
Useful variants:
php artisan maildkim:dns-record --raw
php artisan maildkim:dns-record --pem
php artisan maildkim:dns-record --selector=dkim --domain=example.com
Use --raw when you only need the TXT value, and --pem for debugging or manual inspection of the derived public key.
Sends a real test email using the configured mail transport:
php artisan maildkim:send-test recipient@example.com
Optional sender and subject:
php artisan maildkim:send-test recipient@example.com --from=noreply@example.com --subject="DKIM test"
Use this command for end-to-end validation (transport + signing at send time).
Runs configuration, signature, and DNS checks in one command:
php artisan maildkim:doctor
Optional hints:
php artisan maildkim:doctor --domain=example.com --selector=dkim --to=recipient@example.com
- Service classes use PSR-3 logger injection (
Psr\Log\LoggerInterface) instead of theLogfacade. - This keeps the signing service framework-agnostic, easier to unit test, and still fully compatible with Winter's logger container binding.
This plugin is open source software licensed under MIT.
Make awesome sites with ❄ WinterCMS !
