feat(dkim): add outbound DKIM signing#65
Merged
Merged
Conversation
EvalAlan
force-pushed
the
feat/dkim-signing
branch
from
July 13, 2026 06:03
b8c50c4 to
31f054d
Compare
Sign outbound mail on the remote SMTP delivery path to improve deliverability to Gmail/Yahoo/Outlook. Until now the plugin system only verified DKIM/DMARC/ARC on inbound mail; there was no signing path. - New internal/dkim package wrapping github.com/emersion/go-msgauth/dkim (the same library the verification code references) for both RSA and Ed25519 keys. - [dkim] TOML config section: enabled, relaxed/relaxed canonicalization by default, and per-domain selector / private_key_path / optional headers_to_sign (sensible default header set). - Private-key permission validation on load: keys readable/writable by group or other (0077 bits) are rejected, matching the repo's file-security posture. - Signing wired into SMTPDeliveryHandler, applied once before recipients are grouped by domain. Signing is idempotent (skips a message already signed for the domain) so delivery retries never double-sign. Local LMTP delivery is not signed. - Signing domain selected from the envelope-from domain, falling back to the From header domain (bounces); unconfigured domains are skipped with a debug log and delivered unsigned. - Tests: sign+verify round-trip for RSA and Ed25519 via the verification path, config parsing, key-permission rejection, and retry-no-double-sign (both at the signer and delivery-handler level). - Docs: docs/dkim-signing.md plus a [dkim] example in config/elemta.toml.example.
Reject invalid signed-header configuration at startup, validate DKIM before queue resources are started, and prevent configured-domain signing failures from downgrading delivery to unsigned mail.
EvalAlan
force-pushed
the
feat/dkim-signing
branch
from
July 24, 2026 02:19
5477a89 to
fbd0edf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds outbound DKIM signing. Previously the plugin system only verified DKIM/DMARC/ARC on inbound mail; there was no signing path, which hurts deliverability to Gmail/Yahoo/Outlook. This signs outbound mail on the remote SMTP delivery path.
Design decisions
github.com/emersion/go-msgauth/dkim, the same library referenced by the existing verification code. It supports both signing and verifying with RSA and Ed25519, so the unit tests verify exactly what we sign. No hand-rolled DKIM crypto was added.SMTPDeliveryHandler(remote delivery), not LMTP. Signing is applied inDeliverMessageWithMetadataonce, before recipients are grouped by domain, so identical signed bytes go to every MX. Local LMTP delivery (to Dovecot) is intentionally not signed — a locally delivered copy never crosses an administrative boundary where DKIM matters. The signer is attached viaSetDKIMSigner, wired up ininitQueueSystem.Signer.Signis idempotent: if the content already carries aDKIM-Signaturefor the selected domain (folded continuation lines handled), it returns the content unchanged. A delivery retry therefore never adds a second signature. This is covered by tests at both the signer and delivery-handler level.Fromheader domain when the envelope-from is empty (e.g. bounces/DSNs with a null return path). If no key is configured for the resolved domain, the message is delivered unsigned with a debug log — never failed.0077bits) is rejected, matching the repo's existing file-security checks. Keys are validated up-front inNewSignerso a misconfiguration fails fast at start-up.[dkim]section:enabled,header_canonicalization/body_canonicalization(defaultrelaxed/relaxed), and per-domain[[dkim.domains]]blocks withdomain,selector,private_key_path, and optionalheaders_to_sign(defaults to From, To, Cc, Subject, Date, Message-ID, MIME-Version, Content-Type, Reply-To, In-Reply-To, References).Files changed
internal/dkim/signer.go— new package: config types, key loading (PKCS#1/PKCS#8/SEC1, RSA + Ed25519), permission validation, signing, domain resolution helpers.internal/queue/delivery_handler.go—messageSignerseam,SetDKIMSigner,signContent, sign call inDeliverMessageWithMetadata.internal/config/config.go,internal/smtp/config.go—[dkim]config field.cmd/elemta/commands/server.go,internal/smtp/server.go— build the signer and attach it to the remote SMTP handler.internal/dkim/signer_test.go,internal/config/dkim_config_test.go,internal/queue/delivery_dkim_test.go.docs/dkim-signing.md, section indocs/configuration.md,[dkim]example inconfig/elemta.toml.example.Testing
go build ./...,go vet ./...— clean.gofmt -l .— empty.go test ./...— full suite passes.Tests include RSA and Ed25519 sign→verify round-trips through the verification path, config parsing, insecure-key-permission rejection, and retry-does-not-double-sign at both layers.