<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tomatoes.run]]></title><description><![CDATA[Tomatoes.run]]></description><link>https://tomatoesrun.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a685a032144c4aca5dc25b5/521cab5e-cf6f-4fc6-8bf6-0d7c27fe4d57.png</url><title>Tomatoes.run</title><link>https://tomatoesrun.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 16 Sep 2026 16:35:36 GMT</lastBuildDate><atom:link href="https://tomatoesrun.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Catch-all email aliasing: a different address for every service (Haraka + SRS + DKIM)]]></title><description><![CDATA[Most people use the same email address everywhere. One breach and it leaks for
years. The known fix is a different alias per service — but every tool I tried
made me create each alias upfront. I wante]]></description><link>https://tomatoesrun.hashnode.dev/catch-all-email-aliasing-a-different-address-for-every-service-haraka-srs-dkim</link><guid isPermaLink="true">https://tomatoesrun.hashnode.dev/catch-all-email-aliasing-a-different-address-for-every-service-haraka-srs-dkim</guid><category><![CDATA[email]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[smtp]]></category><category><![CDATA[SaaS]]></category><dc:creator><![CDATA[hashnode]]></dc:creator><pubDate>Tue, 28 Jul 2026 07:42:16 GMT</pubDate><content:encoded><![CDATA[<p>Most people use the same email address everywhere. One breach and it leaks for
years. The known fix is a different alias per service — but every tool I tried
made me <strong>create each alias upfront</strong>. I wanted the opposite: a subdomain where
<em>every</em> address just works, and the alias is born the moment the first email
arrives.</p>
<p>This post is the engineering behind that: how to run a <strong>catch-all subdomain</strong>
mail flow without becoming an open relay, and how to forward mail without
nuking your deliverability (SPF, SRS, DKIM, DMARC). It's what powers
<a href="https://www.tomatoes.run">Tomatoes.run</a> in production, but the ideas apply to
any forwarding setup.</p>
<h2>The core idea: decide at RCPT time, not at signup</h2>
<p>Instead of a table of pre-created aliases, you give each user a personal
subdomain — <code>you.example.com</code> — and treat <strong>every</strong> local-part as potentially
valid: <code>amazon@you.example.com</code>, <code>github@you.example.com</code>, anything. The
validity decision happens <strong>when the mail is received</strong>, not when an alias is
created.</p>
<p>The stack:</p>
<ul>
<li><strong>Haraka</strong> (Node.js SMTP server) as the MX.</li>
<li>A small internal <strong>HTTP API</strong> (Next.js route) that owns the
forward / reject / tempfail decision.</li>
<li><strong>Postgres</strong> for users, aliases and <em>metadata only</em> — message content is
never stored, it's forwarded immediately.</li>
</ul>
<pre><code>inbound mail ──&gt; Haraka (MX) ──hook_rcpt──&gt; internal API ──&gt; decision
                                                              │
                        forward ◄─ rewrite envelope (SRS) ────┘
                        + DKIM sign ──&gt; outbound queue ──&gt; user's real inbox
</code></pre>
<h2>Challenge 1 — a catch-all that isn't an open relay</h2>
<p>The scary part of "accept any recipient" is accidentally relaying spam. The
trick is to be optimistic only for addresses you <em>own</em>, and <strong>fail closed</strong> for
everything else.</p>
<p>In Haraka's <code>hook_rcpt</code>:</p>
<pre><code class="language-js">exports.hook_rcpt = async function (next, connection, params) {
  const rcpt = params[0];
  const domain = rcpt.host.toLowerCase();

  // Our own catch-all subdomains: accept optimistically, resolve later.
  if (domain.endsWith('.example.com')) return next(OK);

  // Custom domains (bring-your-own): must be verified. Ask the API,
  // with a short-TTL cache to avoid a round-trip per RCPT.
  try {
    const known = await isVerifiedDomain(domain); // API + cache
    return next(known ? OK : DENY);
  } catch (err) {
    // API down? DENYSOFT (4xx) — the sender retries, we lose nothing,
    // and we never relay something we couldn't validate. Fail CLOSED.
    return next(DENYSOFT);
  }
};
</code></pre>
<p>Two things matter here:</p>
<ul>
<li><strong><code>DENYSOFT</code> (a 4xx tempfail), not <code>DENY</code></strong>, 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.</li>
<li>The custom-domain gate is what keeps you off "open relay" lists. No
verification, no acceptance.</li>
</ul>
<h2>Challenge 2 — forwarding breaks SPF, so rewrite the envelope (SRS)</h2>
<p>Naive forwarding looks like this: mail comes in for <code>you</code>, you resend it to the
user's real inbox keeping the original <code>MAIL FROM</code>. The receiving MX checks
<strong>SPF</strong> on that <code>MAIL FROM</code> domain… and sees <em>your</em> server sending on behalf of
someone else's domain → <strong>SPF fail</strong> → spam folder or reject.</p>
<p>The fix is <strong>SRS (Sender Rewriting Scheme)</strong>: rewrite the envelope sender to
your own domain, encoded so bounces can be reversed back to the original
sender.</p>
<pre><code class="language-js">// forward:  bob@gmail.com  -&gt;  SRS0=hash=tt=gmail.com=bob@example.com
const bounce = srs.forward(originalMailFrom, 'example.com');

// on a bounce hitting SRS0=... @example.com, reverse it back:
const original = srs.reverse(bounceRecipient); // -&gt; bob@gmail.com
</code></pre>
<p>Now SPF is checked against <em>your</em> domain, which <em>does</em> authorize your server.
The hash makes the token tamper-proof and reversible, so DSNs still reach the
real sender. Keep the SRS secret stable — rotating it invalidates in-flight
bounce addresses.</p>
<h2>Challenge 3 — DKIM-sign outbound, per domain</h2>
<p>Even with SPF happy, unsigned forwarded mail is suspicious. So the outbound
message is <strong>DKIM-signed</strong> with your domain (<code>d=example.com</code>).</p>
<p>The interesting case is <strong>bring-your-own-domain</strong>. When a user adds their own
domain, you generate an RSA keypair, store the private key, and publish the
public key as their DNS TXT record. Outbound mail for that user then gets
signed <strong>twice</strong>:</p>
<ul>
<li><code>d=example.com</code> on the SRS return-path (envelope alignment), and</li>
<li><code>d=theircustomdomain.com</code> on the visible <code>From:</code> (author-domain alignment),</li>
</ul>
<p>so <strong>DMARC</strong> passes on the domain that actually appears in the headers. Two
signatures, one message — a small additive <code>queue_outbound</code> hook that reuses the
DKIM signing stream.</p>
<h2>Challenge 4 — the "no pre-creation" magic</h2>
<p>Back at the API, the decision endpoint is where the product logic lives:</p>
<pre><code class="language-js">// POST /internal/email/receive  { alias, domain, sender, ... }
// returns one of: forward | reject | tempfail
</code></pre>
<ul>
<li>Unknown-but-valid recipient → <strong>auto-create the alias</strong> on first email and
<code>forward</code>. The alias simply <em>appears</em> in the dashboard; the user never
created it.</li>
<li>Free-plan cap reached → <code>reject</code> new aliases (existing ones keep working).</li>
<li>Disabled alias → <code>reject</code>, so leaked addresses go silent at the server edge.</li>
<li>Only <strong>metadata</strong> is recorded (sender, date, size, status). The body is
streamed straight through, never persisted.</li>
</ul>
<p>That single "decide at receive time" inversion is what removes the
create-an-alias-first step entirely.</p>
<h2>Gotchas worth knowing</h2>
<ul>
<li><strong>tempfail vs reject semantics</strong>: use 4xx when <em>you</em> might be wrong
(dependency down), 5xx only when the address is genuinely invalid/blocked.</li>
<li><strong>DMARC alignment</strong> is about the <em>header From</em>, not the envelope — hence the
per-domain DKIM signature above.</li>
<li><strong>Catch-all + spam</strong>: every address existing means every address can be
spammed. Per-alias disable (server-side reject) is the escape hatch.</li>
<li><strong>Don't store content.</strong> It's less liability and, honestly, a better privacy
story — you only ever hold metadata.</li>
</ul>
<hr />
<p>This runs in production as <a href="https://www.tomatoes.run">Tomatoes.run</a>, a
France/EU-hosted take on per-service email aliases (independent, GDPR by
design). If you've fought SPF/DKIM/SRS on forwarding before, I'd love your war
stories in the comments — deliverability is a rabbit hole and I'm still digging. 🍅</p>
]]></content:encoded></item></channel></rss>