Notifio is a desktop app that watches rental search pages and emails you when a new listing appears. Mail is most of the product, so the website holds the whole outbound side: four alert emails, an activation email, a free-licence email and the contact form. I grepped the repository for @notifio.app and found seven distinct addresses, which is three more than I expected.
address sends receives on the site noreply@notifio.app yes yes /help, 10 times support@notifio.app no yes 5 pages, 30 times no-reply@notifio.app no yes no hello@notifio.app no yes no notifications@notifio.app no yes no ceo@notifio.app no yes no alerts@notifio.app no no the homepage screenshot Every row in that table is a decision except the last one.
Four constants, one of them used
export const EmailAddresses = { NOREPLY: "Notifio <noreply@notifio.app>", HELLO: "Notifio <hello@notifio.app>", SUPPORT: "Notifio Support <support@notifio.app>", NOTIFICATIONS: "Notifio <notifications@notifio.app>", } as const; Six sendEmail call sites in the codebase. All six pass EmailAddresses.NOREPLY. The other three constants have zero references anywhere, which makes them a claim about the system rather than part of it: a reader reasonably concludes that support replies and alert mail come from different addresses, and they do not.
There is a seventh send path, in the script that grants a free licence by hand, and it does not use the constant at all:
const FROM = "Notifio <noreply@notifio.app>"; That is because the script is an .mjs file that builds its own Resend client rather than importing the module with the shared one, which is a defensible shortcut with a cost that is exactly one string: the only address this product ever sends from is typed out in two places.
The inbound side, and a label that cannot happen
Mail to the domain arrives through a webhook that forwards it on, and it starts with an allowlist:
const allowedAddresses = [ "ceo@notifio.app", "hello@notifio.app", "support@notifio.app", "notifications@notifio.app", "noreply@notifio.app", "no-reply@notifio.app", ]; if (!allowedAddresses.includes(firstRecipient)) { console.log(`[resend-webhook] Ignoring email to unauthorized address: ${firstRecipient}`); return; // Silently ignore unauthorized addresses } Then it labels the message so the forwarded copy is filterable:
let category = "General"; if (firstRecipient.includes("ceo@")) { category = "CEO"; } else if (firstRecipient.includes("support@")) { category = "Support"; } else if (firstRecipient.includes("hello@")) { category = "Contact"; } else if (firstRecipient.includes("notifications@")) { category = "Notifications"; } else if (firstRecipient.includes("noreply@") || firstRecipient.includes("no-reply@")) { category = "No-Reply"; } The branch list and the allowlist are the same six addresses, written twice, in a different order, with different matching rules: one is an exact comparison and the other is a substring test. And "General" is unreachable. Nothing can get past the allowlist without matching one of the five branches, so the default exists only as evidence that the two lists were written at different times and have agreed with each other ever since by hand.
One object removes both problems:
const INBOX: Record<string, string> = { "ceo@notifio.app": "CEO", "hello@notifio.app": "Contact", "support@notifio.app": "Support", "notifications@notifio.app": "Notifications", "noreply@notifio.app": "No-Reply", "no-reply@notifio.app": "No-Reply", }; const category = INBOX[firstRecipient]; if (!category) return; // not one of ours The allowlist becomes the keys, the labels become the values, a new address is one line instead of two edits in two styles, and the unreachable default stops existing because there is nothing left to default to.
The address in our own screenshot
The homepage has an animated mock of the alert email landing in an inbox. It names its sender twice:
<p className="text-[9px] text-[#a3a3a3]">Notifio Alert <span className="text-[#3a3a3a]"><alerts@notifio.app></span></p> <p className="text-[8px] text-[#2a2a2a]">Sent by Notifio · alerts@notifio.app · Unsubscribe</p> alerts@notifio.app is not in the sending code and it is not in the inbound allowlist. Real alert mail comes from noreply@, and a reply to the address in the picture matches nothing, so it is dropped by the line above with a log entry nobody reads.
The same two lines promise an unsubscribe link, and the word "unsubscribe" appears zero times in the four real alert templates. That part is defensible on its own terms: these are transactional messages your own machine asked us to send about your own saved search, and there is no list to leave, only a monitor to stop in an app you are holding. But a screenshot that shows a footer the product does not have is still a screenshot I have to go and fix, and the fix for both findings is the same: put the real From and the real footer in the mock, because the real ones are not worse.
The sixth email lives outside the email folder
Three files hold the templates, with eleven builder functions between them: the four alert emails and their eight functions, the activation email, and the free-licence email. The contact form's email is not one of them. It is about 60 lines of inline HTML in the route handler, and it carries its own copy of the escaper:
function escHtml(str: string): string { return String(str) .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """); } Which is the same five lines that already exist in the alert template module, for the same reason: everything in these messages is text somebody typed, and we escape it ourselves rather than pulling in a dependency. Having it twice is how a careful decision turns into a careless one.
It is also the only email in the system that comes to us instead of going to a customer, which is why it ended up in the route in the first place and why nobody minded. Its recipient is a personal mailbox typed into the handler rather than any of the four constants, with replyTo set to whoever filled the form in so that answering works. The address it should use is SUPPORT, which is already received and already forwarded by the webhook above, and which would make the one unused constant the one that matters.
The contact form itself is unchanged and I still like it: no captcha and four other things standing in its place.
The last count in the census is my favourite
Which pages name which address:
page support@ noreply@ /help 0 10 /privacy 8 0 /terms 8 0 / 6 0 /about 4 0 /alerts 4 0 /help is the one page with a contact form on it, and the only page that never gives you an address. It names noreply@ ten times instead, because the FAQ answer for "I cannot find my activation email" tells you what to search your inbox for. The five pages that cannot take a message are the pages that hand you somewhere to write to.
That is not a bug and I am not changing it, but it is a good description of what an address census is for. Nothing here was broken. Seven addresses, one sender, two spellings of it, a label that cannot be reached, a screenshot quoting an address that does not exist, and a template in the wrong folder, all of it found by grepping for an @ and then asking of each hit: can this send, can this receive, and does anybody see it?
Notifio is at notifio.app, with the alert setup guides at notifio.app/alerts, the help page and its form at notifio.app/help, what we store and for how long in the privacy notice, and the one-time licence that the activation email exists to deliver.
