Interceptor Address Normalization#
In Bamboo, interceptors run after address normalization, not before. This ordering has a practical consequence: by the time an interceptor's call/1 receives the email, all recipient fields (to, cc, bcc, from) have already been reduced from their original forms (e.g., rich domain structs) to {name, address} tuples via Bamboo.Formatter.
Execution Order in deliver_now/4#
The deliver_now/4 pipeline runs in this sequence:
validate_and_normalize/2β validates the email (from address, recipients, attachment support), then callsnormalize_addresses/1to convert all address fields to{name, address}tuples via theBamboo.Formatterprotocol.apply_interceptors/2β iterates the configuredinterceptorslist, calling each module'scall/1in order.- If not blocked and recipients are non-empty, the email is dispatched to the adapter.
The same order applies in deliver_later/3.
What Normalization Does#
normalize_addresses/1 rewrites the from, to, cc, and bcc fields by passing each through Formatter.format_email_address/2. For any rich struct (e.g., a User schema), all fields except the email address and display name are dropped. A plain string like "foo@bar.com" becomes {nil, "foo@bar.com"}. Custom structs must implement the Bamboo.Formatter protocol to control their own conversion.
The Interceptor Limitation (and Proposed Fix)#
Because normalization runs first, interceptors cannot access the original struct. For example, a DenyListInterceptor that checks email.to will see a list of {name, address} tuples β not User structs β making it impossible to branch on arbitrary struct fields like preferences or roles.
The test-suite DenyListInterceptor illustrates this: it checks email.to using elem(&1, 1) to extract the address string from already-normalized tuples. The Bamboo.Interceptor docs show the same assumption β the example checks email.to in @deny_list expecting normalized string/tuple values.
PR #682 proposes reversing this order β running apply_interceptors before validate_and_normalize β so interceptors can access the richer pre-normalized structs. As of this writing the PR is open and unmerged; the current codebase still normalizes first.
Implications for Interceptor Authors#
| Need | Workaround (current) |
|---|---|
| Block by email address | Use elem(recipient, 1) on normalized tuples |
| Branch on struct fields | Implement logic in Bamboo.Formatter instead, or store metadata in email headers/assigns before normalization |
| Access original struct | Not possible without merging PR #682 or patching deliver_now locally |
Only one interceptor config key exists: interceptors. There is no before_interceptors key in the current codebase.
Key Files#
| File | Role |
|---|---|
lib/bamboo/mailer.ex | deliver_now/4, normalize_addresses/1, apply_interceptors/2 |
lib/bamboo/interceptor.ex | Bamboo.Interceptor behaviour definition |
test/support/deny_list_interceptor.ex | Reference interceptor operating on normalized tuples |
| PR #682 | Proposed swap: interceptors before normalization |