← Liana Grigory
Insights

The email you do not send

Every throwaway test account is a real message to an address that does not exist. The bounces land in somebody's inbox, and the sender reputation they burn is the one the password-reset mail depends on.

By Liana Grigory · 24 September 2026 · 8 min read

I left two test accounts alive on a production auth tenant. They were called something like [email protected], they existed to prove a screen rendered, and I did not delete them at the end of the session. Over the following days the owner's inbox filled with non-delivery reports, because example.com does not accept mail and never will.

The embarrassing part is not the mess. It is that I had reasoned about the account and not at all about the side effect. Creating a user through a sign-up form is not a read-only act of inspection. It is an outbound message to a real mail system, sent from a domain whose reputation other people depend on.

Why an invented address is worse than it looks

example.com, example.org and .test are reserved precisely so that nobody accidentally routes traffic to a stranger. The consequence is that every message to them is a hard bounce: not a delay, not a soft failure, a permanent rejection.

Hard bounces are the input to sender reputation. Mailbox providers treat a stream of messages to addresses that do not exist as the signature of a list that was never opted into, because that is usually what it is. The penalty is not applied to the test messages. It is applied to the domain, and therefore to everything else that domain sends.

Which is the actual cost, and it takes a moment to land. The same domain sends the verification email and the password reset. So the mail that gets throttled or filed as spam is the one going to somebody who is locked out of their account right now and cannot get in without it. I traded a sixty-second convenience against the delivery of the single most time-critical message the product sends.

Invented addresses on a domain that does exist are worse still, because the bounce either lands on a stranger's mail server or, if the address happens to resolve, the message lands in a stranger's inbox.

Where the send actually comes from

This is the detail that makes the problem solvable, and I did not know it when I caused the problem.

Firebase Auth does not send a verification email when a user is created. The application does, by calling sendEmailVerification() after sign-up, usually one line below the call that created the account. The sign-up UI is the sender. The auth service is not.

So the Identity Toolkit REST endpoint that the SDK itself wraps — accounts:signUp — creates a user, returns a localId and an idToken, and sends nothing at all. There is no flag to suppress. There is simply no send, because the send was always the application's decision.

One wrinkle worth knowing: if the web API key is restricted by HTTP referrer, which it should be, the request needs a Referer header matching the real site. That is not a bypass of the restriction. It is the restriction working exactly as designed — the key is public, and the referrer check is what makes its publicness acceptable.

Marking the address confirmed is a separate administrative call against the project, authorised with a Google Cloud access token rather than the web key, setting emailVerified directly. No confirmation message is generated, because nothing is being confirmed to anybody.

Four ways to test the account layer, in order of preference

1. Do not create an account. Most of the questions that seem to need one do not. If the question is whether a security rule permits a read, the rules service will evaluate the rule against a synthetic request and a synthetic resource and return a boolean. No account, no document, no data, no cost, and a far more precise answer than a manual click-through, because you can assert the denial as well as the permission. I have wasted hours creating users to check something a rule evaluation settles in seconds.

2. Read the code. "What does this screen show a user in state X" is answered by the component, deterministically, without provisioning anything. Reaching for a live account to answer a question the source answers is a habit worth breaking on cost grounds alone.

3. Reuse a durable QA account. If the flow genuinely needs a session, a small set of long-lived accounts on addresses that really deliver, documented somewhere the next session will find them, sends no mail because the account already exists. The failure mode here is forgetting they exist and minting a new one every time, which is how you end up with fourteen of them.

4. Create one by REST, and delete it in the same run. When nothing else will do: create it without the UI so nothing is sent, do the one thing you needed, then delete the user and every document it wrote. The deletion is not tidiness, it is part of the creation. An orphaned test profile on a public marketplace is a worse artefact than a bounce, because it is visible to customers and it is indexable.

When a real mailbox is genuinely required

Sometimes the thing under test is the mail itself — the template renders, the link in it works, the token is consumed once. That cannot be proven without delivery.

The answer is a plus-addressed mailbox on a domain you control: [email protected]. It is deliverable, it arrives in a real inbox, it filters into a folder, and it never bounces. It costs nothing and it is not a workaround, it is just addressing. The only discipline required is saying in the report that a message was sent and to where, so the person who owns the inbox is not surprised by it.

The general shape

The mistake generalises well beyond email, which is why I think it is worth writing down rather than filing as carelessness.

A test that produces an outbound side effect is not a test. It is a small production event with a test's expectations attached to it. The same reasoning applies to a webhook that fires at a third party, an SMS that costs money and annoys a real handset, a push notification to whichever devices happen to be registered, and anything that writes to a payment processor. In each case the local result looks identical whether or not the side effect was appropriate, because the side effect happens somewhere you are not looking.

So the question I now ask before provisioning anything is not "is this account safe to create" but "what leaves this system when I do". If something leaves, the test needs a different design: evaluate the rule instead of the session, read the source instead of the screen, reuse the fixture instead of minting one, and if none of that works, own the deletion at the same moment you own the creation.

The version of me that left two accounts on example.com was not being reckless. I was being efficient about the wrong boundary — optimising the thing I could see, which was the account, and ignoring the thing I could not, which was the message on its way out of the building.

Written by Liana Grigory, Entrepreneur and Software Engineer, from work on The Care Royal, Tegula Stone and Unified Savers. Everything above describes decisions actually made on those systems, including the ones that turned out to be wrong.

Home · Terms of Use · Privacy Notice