<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Houzelle</title>
    <description>The latest articles on DEV Community by Houzelle (@eric-houz).</description>
    <link>https://dev.to/eric-houz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4050735%2F12422937-2508-41ea-992b-559f0b590f0f.webp</url>
      <title>DEV Community: Houzelle</title>
      <link>https://dev.to/eric-houz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eric-houz"/>
    <language>en</language>
    <item>
      <title>Multi-tenant DKIM in Haraka: two signatures on one message</title>
      <dc:creator>Houzelle</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:52:24 +0000</pubDate>
      <link>https://dev.to/eric-houz/multi-tenant-dkim-in-haraka-two-signatures-on-one-message-46b4</link>
      <guid>https://dev.to/eric-houz/multi-tenant-dkim-in-haraka-two-signatures-on-one-message-46b4</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/eric-houz/catch-all-email-aliasing-a-different-address-for-every-service-haraka-srs-dkim-545a"&gt;previous post&lt;/a&gt;&lt;br&gt;
I described a catch-all email forwarder built on Haraka, and I waved at the&lt;br&gt;
bring-your-own-domain case with one lazy sentence: &lt;em&gt;"a small additive&lt;br&gt;
&lt;code&gt;queue_outbound&lt;/code&gt; hook that reuses the DKIM signing stream."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That sentence hid the hardest part of the whole system. This post is the part I&lt;br&gt;
skipped: how to DKIM-sign outbound mail for &lt;strong&gt;hundreds of customer domains&lt;/strong&gt;,&lt;br&gt;
with per-tenant keys living in a database, from a single Haraka instance. It's&lt;br&gt;
what powers custom domains on &lt;a href="https://www.tomatoes.run" rel="noopener noreferrer"&gt;Tomatoes.run&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why one signature isn't enough
&lt;/h2&gt;

&lt;p&gt;Forwarding mail means rewriting the envelope sender (SRS) so SPF passes on&lt;br&gt;
&lt;em&gt;your&lt;/em&gt; domain. Fine. So you DKIM-sign with &lt;code&gt;d=example.com&lt;/code&gt; and call it a day.&lt;/p&gt;

&lt;p&gt;Then a customer points &lt;code&gt;mail.theircompany.com&lt;/code&gt; at you, and DMARC breaks.&lt;/p&gt;

&lt;p&gt;The reason is &lt;strong&gt;alignment&lt;/strong&gt;. DMARC doesn't check the envelope — it checks the&lt;br&gt;
domain in the visible &lt;code&gt;From:&lt;/code&gt; header, and demands that &lt;em&gt;either&lt;/em&gt; SPF &lt;em&gt;or&lt;/em&gt; DKIM&lt;br&gt;
pass &lt;strong&gt;for that domain&lt;/strong&gt;. After SRS, your envelope says &lt;code&gt;example.com&lt;/code&gt; while the&lt;br&gt;
header still says &lt;code&gt;theircompany.com&lt;/code&gt;. Nothing aligns with the header domain, so&lt;br&gt;
DMARC fails even though both SPF and DKIM technically "pass".&lt;/p&gt;

&lt;p&gt;You need a signature whose &lt;code&gt;d=&lt;/code&gt; is the customer's domain. And you still need the&lt;br&gt;
first one, for the SRS return-path. So: &lt;strong&gt;two signatures on one message.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — mark the message on the way in
&lt;/h2&gt;

&lt;p&gt;DKIM signing happens late, at queue time. The decision about &lt;em&gt;which&lt;/em&gt; domain to&lt;br&gt;
sign for is made much earlier, when you know the recipient. So you stash it on&lt;br&gt;
the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tell haraka-plugin-dkim this is a relayed message: it signs with the&lt;/span&gt;
&lt;span class="c1"&gt;// envelope domain (SRS-rewritten to ours), not the original From — we&lt;/span&gt;
&lt;span class="c1"&gt;// have no key for that one.&lt;/span&gt;
&lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Custom domain? Ask for a SECOND signature with d=&amp;lt;their domain&amp;gt;.&lt;/span&gt;
&lt;span class="c1"&gt;// Our own catch-all subdomains (you.example.com) are excluded: they're&lt;/span&gt;
&lt;span class="c1"&gt;// already covered by the envelope signature.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rcptHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rcpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcptHost&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rcptHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sign_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rcptHost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exclusion matters. Signing &lt;code&gt;d=you.example.com&lt;/code&gt; would need a key per&lt;br&gt;
subdomain, and there's no point: the envelope signature on the parent domain&lt;br&gt;
already aligns for those.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — add the second signature at queue_outbound
&lt;/h2&gt;

&lt;p&gt;Here's the trick, and it depends on &lt;strong&gt;plugin order&lt;/strong&gt;. Your custom hook must run&lt;br&gt;
&lt;em&gt;after&lt;/em&gt; &lt;code&gt;haraka-plugin-dkim&lt;/code&gt; (order is set in &lt;code&gt;config/plugins&lt;/code&gt;), so the first&lt;br&gt;
signature already exists and you're purely additive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DKIMSignStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;haraka-plugin-dkim/lib/dkim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hook_queue_outbound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sign_domain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;DKIMSignStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_dkim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// no key provisioned -&amp;gt; ship it anyway&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// never call next() twice&lt;/span&gt;
      &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpipe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DKIMSignStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DKIM_SIGN_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body_canon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relaxed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dkimHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dkimHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DKIM-Signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dkimHeader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reuse &lt;code&gt;DKIMSignStream&lt;/code&gt; instead of reimplementing signing.&lt;/strong&gt; It's an internal
path of &lt;code&gt;haraka-plugin-dkim&lt;/code&gt;, so I &lt;code&gt;require&lt;/code&gt; it inside a &lt;code&gt;try/catch&lt;/code&gt; and leave
the binding &lt;code&gt;null&lt;/code&gt; on failure. If upstream reshuffles its files, custom
signing quietly stops — mail still flows, signed with the envelope domain.
A missing feature beats a crashed MX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;done&lt;/code&gt; flag is not paranoia.&lt;/strong&gt; A stream that both errors &lt;em&gt;and&lt;/em&gt; fires its
callback would call &lt;code&gt;next()&lt;/code&gt; twice, and Haraka will happily queue the message
twice. Guard it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3 — per-tenant keys
&lt;/h2&gt;

&lt;p&gt;Each verified domain gets an RSA keypair at signup: public half goes in the&lt;br&gt;
customer's DNS as &lt;code&gt;&amp;lt;selector&amp;gt;._domainkey&lt;/code&gt;, private half into Postgres.&lt;/p&gt;

&lt;p&gt;Haraka fetches it through a read-only internal endpoint, authenticated with the&lt;br&gt;
same shared secret as the rest of the internal API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /internal/email/dkim  { domain } -&amp;gt; { selector, privateKey } | { key: null }&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;not&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dkimSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dkimPrivateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dkimSelector&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dkimPrivateKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;verifiedAt: { not: null }&lt;/code&gt; — an unverified domain never gets a key served,&lt;br&gt;
which is the same gate that stops the MX from becoming an open relay.&lt;/p&gt;

&lt;p&gt;Keys are cached in-process with a TTL, because otherwise every single message&lt;br&gt;
costs an HTTP round-trip plus a DB read.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I find most interesting: opposite failure modes
&lt;/h2&gt;

&lt;p&gt;Both of these talk to the same internal API. They fail in &lt;strong&gt;opposite&lt;br&gt;
directions&lt;/strong&gt;, on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recipient validation fails closed.&lt;/strong&gt; At &lt;code&gt;hook_rcpt&lt;/code&gt;, if the API is&lt;br&gt;
unreachable, you answer &lt;code&gt;DENYSOFT&lt;/code&gt; — a 4xx tempfail. SMTP is store-and-forward,&lt;br&gt;
so the sender retries for days; a few minutes of downtime loses nothing. Accept&lt;br&gt;
optimistically here and you're an open relay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DENYSOFT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// never accept what you can't validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Signing fails open.&lt;/strong&gt; At &lt;code&gt;hook_queue_outbound&lt;/code&gt;, every error path just calls&lt;br&gt;
&lt;code&gt;next()&lt;/code&gt;. API down, key missing, stream throws, module moved — the message goes&lt;br&gt;
out anyway, already signed with the envelope domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logwarn&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// ship it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asymmetry is the whole design. Failing closed on validation protects&lt;br&gt;
&lt;em&gt;everyone else&lt;/em&gt; from you relaying spam. Failing closed on signing would protect&lt;br&gt;
&lt;em&gt;nobody&lt;/em&gt; — it would just silently stop your customer's mail over a degraded&lt;br&gt;
optional feature. Match the failure direction to who gets hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas and things I'm still unsure about
&lt;/h2&gt;

&lt;p&gt;Being honest about the rough edges, because this is the stuff that costs you a&lt;br&gt;
weekend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Piping &lt;code&gt;message_stream&lt;/code&gt; a second time at queue time is the part I trust
least.&lt;/strong&gt; The first signature already consumed the stream; this hook attaches
another consumer to it. It works in my testing and the &lt;code&gt;unpipe()&lt;/code&gt; in &lt;code&gt;finish()&lt;/code&gt;
keeps it tidy, but I'd call it "validated on my traffic", not "battle-tested at
scale". If you know a cleaner way to get two signatures out of Haraka, the
comments are open — that's half my reason for writing this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative results aren't cached.&lt;/strong&gt; A verified domain with no key provisioned
hits the API on &lt;em&gt;every&lt;/em&gt; message. Easy fix, still on my list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signed-headers list is duplicated&lt;/strong&gt; between the plugin constant and
&lt;code&gt;config/dkim.ini [sign]&lt;/code&gt;. Two places, one meaning, guaranteed to drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private keys sit in Postgres&lt;/strong&gt;, on the same trust boundary as forwarding
addresses. Defensible for a small operation, and honestly the thing I'd move
to a KMS first if this grew.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC is about the header &lt;code&gt;From&lt;/code&gt;&lt;/strong&gt;, not the envelope. If you remember one
sentence from this post, that's the one — almost every forwarding
deliverability bug I've hit traces back to forgetting it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This runs in production as &lt;a href="https://www.tomatoes.run" rel="noopener noreferrer"&gt;Tomatoes.run&lt;/a&gt;, a&lt;br&gt;
France/EU-hosted take on per-service email aliases — every address on your&lt;br&gt;
subdomain works instantly, and Premium users can bring their own domain (so&lt;br&gt;
leaving is just repointing an MX record). If you've done multi-tenant DKIM&lt;br&gt;
differently, I genuinely want to hear it. 🍅&lt;/p&gt;

</description>
      <category>node</category>
      <category>email</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Catch-all email aliasing: a different address for every service (Haraka + SRS + DKIM)</title>
      <dc:creator>Houzelle</dc:creator>
      <pubDate>Tue, 28 Jul 2026 07:21:53 +0000</pubDate>
      <link>https://dev.to/eric-houz/catch-all-email-aliasing-a-different-address-for-every-service-haraka-srs-dkim-545a</link>
      <guid>https://dev.to/eric-houz/catch-all-email-aliasing-a-different-address-for-every-service-haraka-srs-dkim-545a</guid>
      <description>&lt;p&gt;Most people use the same email address everywhere. One breach and it leaks for&lt;br&gt;
years. The known fix is a different alias per service — but every tool I tried&lt;br&gt;
made me &lt;strong&gt;create each alias upfront&lt;/strong&gt;. I wanted the opposite: a subdomain where&lt;br&gt;
&lt;em&gt;every&lt;/em&gt; address just works, and the alias is born the moment the first email&lt;br&gt;
arrives.&lt;/p&gt;

&lt;p&gt;This post is the engineering behind that: how to run a &lt;strong&gt;catch-all subdomain&lt;/strong&gt;&lt;br&gt;
mail flow without becoming an open relay, and how to forward mail without&lt;br&gt;
nuking your deliverability (SPF, SRS, DKIM, DMARC). It's what powers&lt;br&gt;
&lt;a href="https://www.tomatoes.run" rel="noopener noreferrer"&gt;Tomatoes.run&lt;/a&gt; in production, but the ideas apply to&lt;br&gt;
any forwarding setup.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea: decide at RCPT time, not at signup
&lt;/h2&gt;

&lt;p&gt;Instead of a table of pre-created aliases, you give each user a personal&lt;br&gt;
subdomain — &lt;code&gt;you.example.com&lt;/code&gt; — and treat &lt;strong&gt;every&lt;/strong&gt; local-part as potentially&lt;br&gt;
valid: &lt;code&gt;amazon@you.example.com&lt;/code&gt;, &lt;code&gt;github@you.example.com&lt;/code&gt;, anything. The&lt;br&gt;
validity decision happens &lt;strong&gt;when the mail is received&lt;/strong&gt;, not when an alias is&lt;br&gt;
created.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Haraka&lt;/strong&gt; (Node.js SMTP server) as the MX.&lt;/li&gt;
&lt;li&gt;A small internal &lt;strong&gt;HTTP API&lt;/strong&gt; (Next.js route) that owns the
forward / reject / tempfail decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; for users, aliases and &lt;em&gt;metadata only&lt;/em&gt; — message content is
never stored, it's forwarded immediately.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inbound mail ──&amp;gt; Haraka (MX) ──hook_rcpt──&amp;gt; internal API ──&amp;gt; decision
                                                              │
                        forward ◄─ rewrite envelope (SRS) ────┘
                        + DKIM sign ──&amp;gt; outbound queue ──&amp;gt; user's real inbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Challenge 1 — a catch-all that isn't an open relay
&lt;/h2&gt;

&lt;p&gt;The scary part of "accept any recipient" is accidentally relaying spam. The&lt;br&gt;
trick is to be optimistic only for addresses you &lt;em&gt;own&lt;/em&gt;, and &lt;strong&gt;fail closed&lt;/strong&gt; for&lt;br&gt;
everything else.&lt;/p&gt;

&lt;p&gt;In Haraka's &lt;code&gt;hook_rcpt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hook_rcpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rcpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rcpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Our own catch-all subdomains: accept optimistically, resolve later.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Custom domains (bring-your-own): must be verified. Ask the API,&lt;/span&gt;
  &lt;span class="c1"&gt;// with a short-TTL cache to avoid a round-trip per RCPT.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isVerifiedDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// API + cache&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;OK&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DENY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// API down? DENYSOFT (4xx) — the sender retries, we lose nothing,&lt;/span&gt;
    &lt;span class="c1"&gt;// and we never relay something we couldn't validate. Fail CLOSED.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DENYSOFT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DENYSOFT&lt;/code&gt; (a 4xx tempfail), not &lt;code&gt;DENY&lt;/code&gt;&lt;/strong&gt;, when the validating API is
unreachable. SMTP is store-and-forward: the sending server retries for days.
A few minutes of downtime loses zero mail, and you never blindly accept.&lt;/li&gt;
&lt;li&gt;The custom-domain gate is what keeps you off "open relay" lists. No
verification, no acceptance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenge 2 — forwarding breaks SPF, so rewrite the envelope (SRS)
&lt;/h2&gt;

&lt;p&gt;Naive forwarding looks like this: mail comes in for &lt;code&gt;you&lt;/code&gt;, you resend it to the&lt;br&gt;
user's real inbox keeping the original &lt;code&gt;MAIL FROM&lt;/code&gt;. The receiving MX checks&lt;br&gt;
&lt;strong&gt;SPF&lt;/strong&gt; on that &lt;code&gt;MAIL FROM&lt;/code&gt; domain… and sees &lt;em&gt;your&lt;/em&gt; server sending on behalf of&lt;br&gt;
someone else's domain → &lt;strong&gt;SPF fail&lt;/strong&gt; → spam folder or reject.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;SRS (Sender Rewriting Scheme)&lt;/strong&gt;: rewrite the envelope sender to&lt;br&gt;
your own domain, encoded so bounces can be reversed back to the original&lt;br&gt;
sender.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// forward:  bob@gmail.com  -&amp;gt;  SRS0=hash=tt=gmail.com=bob@example.com&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bounce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;srs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalMailFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// on a bounce hitting SRS0=... @example.com, reverse it back:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;srs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bounceRecipient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// -&amp;gt; bob@gmail.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now SPF is checked against &lt;em&gt;your&lt;/em&gt; domain, which &lt;em&gt;does&lt;/em&gt; authorize your server.&lt;br&gt;
The hash makes the token tamper-proof and reversible, so DSNs still reach the&lt;br&gt;
real sender. Keep the SRS secret stable — rotating it invalidates in-flight&lt;br&gt;
bounce addresses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenge 3 — DKIM-sign outbound, per domain
&lt;/h2&gt;

&lt;p&gt;Even with SPF happy, unsigned forwarded mail is suspicious. So the outbound&lt;br&gt;
message is &lt;strong&gt;DKIM-signed&lt;/strong&gt; with your domain (&lt;code&gt;d=example.com&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The interesting case is &lt;strong&gt;bring-your-own-domain&lt;/strong&gt;. When a user adds their own&lt;br&gt;
domain, you generate an RSA keypair, store the private key, and publish the&lt;br&gt;
public key as their DNS TXT record. Outbound mail for that user then gets&lt;br&gt;
signed &lt;strong&gt;twice&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;d=example.com&lt;/code&gt; on the SRS return-path (envelope alignment), and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d=theircustomdomain.com&lt;/code&gt; on the visible &lt;code&gt;From:&lt;/code&gt; (author-domain alignment),&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;so &lt;strong&gt;DMARC&lt;/strong&gt; passes on the domain that actually appears in the headers. Two&lt;br&gt;
signatures, one message — a small additive &lt;code&gt;queue_outbound&lt;/code&gt; hook that reuses the&lt;br&gt;
DKIM signing stream.&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenge 4 — the "no pre-creation" magic
&lt;/h2&gt;

&lt;p&gt;Back at the API, the decision endpoint is where the product logic lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /internal/email/receive  { alias, domain, sender, ... }&lt;/span&gt;
&lt;span class="c1"&gt;// returns one of: forward | reject | tempfail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Unknown-but-valid recipient → &lt;strong&gt;auto-create the alias&lt;/strong&gt; on first email and
&lt;code&gt;forward&lt;/code&gt;. The alias simply &lt;em&gt;appears&lt;/em&gt; in the dashboard; the user never
created it.&lt;/li&gt;
&lt;li&gt;Free-plan cap reached → &lt;code&gt;reject&lt;/code&gt; new aliases (existing ones keep working).&lt;/li&gt;
&lt;li&gt;Disabled alias → &lt;code&gt;reject&lt;/code&gt;, so leaked addresses go silent at the server edge.&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;metadata&lt;/strong&gt; is recorded (sender, date, size, status). The body is
streamed straight through, never persisted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single "decide at receive time" inversion is what removes the&lt;br&gt;
create-an-alias-first step entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tempfail vs reject semantics&lt;/strong&gt;: use 4xx when &lt;em&gt;you&lt;/em&gt; might be wrong
(dependency down), 5xx only when the address is genuinely invalid/blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC alignment&lt;/strong&gt; is about the &lt;em&gt;header From&lt;/em&gt;, not the envelope — hence the
per-domain DKIM signature above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-all + spam&lt;/strong&gt;: every address existing means every address can be
spammed. Per-alias disable (server-side reject) is the escape hatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't store content.&lt;/strong&gt; It's less liability and, honestly, a better privacy
story — you only ever hold metadata.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This runs in production as &lt;a href="https://www.tomatoes.run" rel="noopener noreferrer"&gt;Tomatoes.run&lt;/a&gt;, a&lt;br&gt;
France/EU-hosted take on per-service email aliases (independent, GDPR by&lt;br&gt;
design). If you've fought SPF/DKIM/SRS on forwarding before, I'd love your war&lt;br&gt;
stories in the comments — deliverability is a rabbit hole and I'm still digging. 🍅&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
