·Engineering·14 min read

How disposable email works

A disposable email address looks magical from the outside. Click a button, get a fresh inbox, watch verification codes show up in real time, walk away. Under the hood it is a small stack of well-understood protocols glued together with care. This is the full walkthrough — DNS, SMTP, storage, polling — using Fake Email's open-source Rust implementation as the worked example.

1. What is a disposable email address?

A disposable email address — also called temp mail, throwaway email, burner email, or fake email — is a real, working inbox at a public domain that anyone can claim for a few minutes and then abandon. The critical property is that it is a real RFC 5321 / RFC 5322 inbox: actual SMTP servers around the world will accept RCPT TO: <alice@fake-email.site>, deliver the message, and you will see it in a browser tab.

From the user's point of view, three things matter: you do not sign up, you receive mail almost instantly, and the inbox vanishes when you are done. Behind that is a chain of moving parts — DNS, an inbound SMTP server, a parser, a database, an HTTP API, and a polling UI. Let's walk it.

2. The end-to-end pipeline

When someone sends a message to alice@fake-email.site, this is the path of the envelope from their send button to your browser tab:

Sender's MTA  ──► DNS lookup: MX for fake-email.site
              ──► resolves to mail.fake-email.site (your EC2)
              ──► TCP connection to port 25
              ──► SMTP conversation (HELO, MAIL FROM, RCPT TO, DATA)
              ──► RFC 5322 message bytes received

Rust SMTP server (this codebase)
              ──► parses headers + body, normalizes UTF-8
              ──► writes (mailbox_id, message) row to Postgres

Browser tab on /emails
              ──► HTTP GET /api/inbox/poll?address=alice@fake-email.site
              ──► server reads from Postgres, returns JSON
              ──► UI renders new messages

Background expiry job
              ──► deletes rows older than TTL
              ──► messages + metadata gone, no trace

Every piece has a 50-year-old standards document behind it. The novelty is gluing them together cleanly. Let's zoom in.

3. Step 1 — DNS and the MX record

Email delivery starts with DNS. When a sender's Mail Transfer Agent (Gmail's outbound servers, Postfix at a corporate mail relay, SendGrid's edge — anything) needs to deliver mail to alice@fake-email.site, it asks for the MX (Mail eXchanger) record of fake-email.site:

$ dig MX fake-email.site +short
10 mail.fake-email.site.

The MX response says: “to deliver mail for this domain, connect to mail.fake-email.site on port 25.” A separate A record resolves mail.fake-email.site to the EC2 instance's elastic IP. Two DNS records — that is the whole “routing layer.”

Two things have to be right at this layer or nothing else matters:

  • The MX record points to a real hostname (not a CNAME, not an IP — RFC 5321 §5.1 forbids both).
  • Port 25 inbound is open at the cloud provider. AWS blocks port 25 by default; you have to file a request to lift the limit before the SMTP server can ever receive a packet.

4. Step 2 — The inbound SMTP server

Once a TCP connection arrives on port 25, the SMTP conversation begins. SMTP is line-based and amusingly chatty. A real exchange looks like this (lines starting < are ours, lines starting > are the sender):

< 220 mail.fake-email.site Service ready
> EHLO sender.example.com
< 250-mail.fake-email.site
< 250 SIZE 10485760
> MAIL FROM:<bob@example.com>
< 250 OK
> RCPT TO:<alice@fake-email.site>
< 250 OK
> DATA
< 354 Start mail input; end with <CRLF>.<CRLF>
> From: bob@example.com
> To: alice@fake-email.site
> Subject: Your verification code
>
> Your code is 482910.
> .
< 250 OK: queued
> QUIT
< 221 Bye

Fake Email's SMTP server is a Rust binary in crates/smtp/. It listens on port 25, accepts the conversation, validates that the RCPT TO domain matches our configured domain, and reads the full message bytes after the DATA command. That is the minimum viable inbound MTA.

For a production temp-mail service you also want:

  • Per-IP rate limiting at this layer to slow down spam blasts before they hit the DB.
  • SIZE advertisement so senders self-truncate huge bodies before transmitting.
  • Connection timeouts on every state — a slow sender shouldn't pin a worker.
  • STARTTLS if you want sender-side encryption (not strictly required for receive-only public temp-mail, but increases delivery reputation).

5. Step 3 — Parsing and storing the message

The SMTP server hands the raw message bytes — an RFC 5322 message — to a parser. RFC 5322 defines the headers (From, To, Subject, Date, Message-ID) plus the body. MIME (RFC 2045–2049) extends the body to allow attachments, HTML, alternative encodings, and character set declarations. A robust parser must handle:

  • Folded headers (header values broken across multiple physical lines).
  • Content-Transfer-Encoding: quoted-printable and base64.
  • Multiple character sets — UTF-8, ISO-8859-1, Windows-1252, sometimes worse.
  • Multipart bodies: multipart/alternative for HTML+text, multipart/mixed for attachments.
  • Hostile bodies. Some senders ship malformed MIME boundaries hoping you crash.

Once parsed, the server writes a row to Postgres:

INSERT INTO messages (
  id, mailbox_address, sender, subject, body_text, body_html,
  attachments, received_at, expires_at
) VALUES (
  gen_random_uuid(),
  'alice@fake-email.site',
  'bob@example.com',
  'Your verification code',
  'Your code is 482910.',
  NULL,
  '[]'::jsonb,
  now(),
  now() + interval '15 minutes'
);

Note the expires_atcolumn: the message's self-destruct time is decided at insert. There is no separate decision needed at expiry — a background job just deletes anything past its TTL.

6. Step 4 — Real-time polling from the browser

With the message safely in Postgres, the user's open browser tab is the consumer. Fake Email uses straightforward HTTP polling against GET /api/inbox/poll?address=alice@fake-email.site. The HTTP server in crates/http-server/ reads:

SELECT id, sender, subject, body_text, body_html, attachments, received_at
FROM messages
WHERE mailbox_address = $1
ORDER BY received_at DESC;

The client (the EmailsPage React component) polls this every few seconds. The UI diffs the returned list against what it has rendered and slides new messages into view. We picked HTTP polling over WebSockets or SSE for three reasons:

  • Stateless. Any HTTP server behind any load balancer answers any request — no per-connection state to migrate.
  • Cache-friendly. The endpoint is cacheable for very short windows (sub-second), which absorbs hot tabs without DB pressure.
  • Robust to client hibernation. Closed laptop lids and reopened tabs just resume polling; no reconnect handshake.

For higher fanout you would layer Postgres LISTEN/NOTIFY and SSE on top. We have not needed it yet.

7. Step 5 — Expiry and cleanup

Expiry is the whole point of a disposable email service. We delete aggressively:

-- runs every minute via a background tokio task
DELETE FROM messages WHERE expires_at < now();
DELETE FROM mailboxes WHERE last_seen_at < now() - interval '1 hour';

Once a row is deleted, the bytes are gone from active storage. WAL and replicas rotate them out within hours. There is no “archive” tier, no “cold storage,” no analytics warehouse fed by these tables. The data was always meant to die.

8. Doing all of this from code

Everything the website does, the public REST API does. Most people think “temp mail” and picture a webpage. Developers should picture two HTTP calls.

# 1. create a mailbox
curl -X POST https://fake-email.site/api/temporary-address \
     -H 'content-type: application/json' \
     -d '{"username":"alice"}'
# { "temp_email_addr": "alice@fake-email.site" }

# 2. paste the address somewhere, then poll
curl 'https://fake-email.site/api/inbox/poll?address=alice@fake-email.site'
# { "messages": [ { "subject": "Your code", "body_text": "..." } ] }

In JavaScript / Playwright / Cypress for end-to-end email verification tests:

const res = await fetch("https://fake-email.site/api/temporary-address", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({}),
});
const { temp_email_addr } = await res.json();

await page.fill('input[name="email"]', temp_email_addr);
await page.click('button[type="submit"]');

// poll until the OTP arrives
for (let i = 0; i < 30; i++) {
  const inbox = await fetch(
    `https://fake-email.site/api/inbox/poll?address=${temp_email_addr}`,
  ).then((r) => r.json());
  const msg = inbox.messages.find((m) => /Your code is/.test(m.body_text));
  if (msg) {
    const code = msg.body_text.match(/\d{6}/)?.[0];
    await page.fill('input[name="otp"]', code);
    break;
  }
  await new Promise((r) => setTimeout(r, 1000));
}

Full reference: /docs/api. Machine-readable OpenAPI 3.1 spec: /openapi.json.

9. Privacy, abuse, and what we don't store

Two questions any temp-mail user should be able to answer at a glance:

What gets stored?

  • The mailbox address (until expiry).
  • The received messages (until expiry).
  • Nothing tied to a person — there is no account, no signup, no profile.

What does NOT get stored?

  • No user account. No password. No name. No phone.
  • No third-party analytics, no fingerprinting library, no ad tags.
  • No long-term retention. After expiry rows are deleted; WAL rotates.

What about abuse?

The service is receive-only — you cannot send mail through Fake Email. That single restriction shuts down the biggest abuse vector (using temp-mail as an open relay). For other kinds of abuse — using disposable addresses to evade bans, abuse trial systems, etc. — the website where you signed up is the right enforcement point, not us.

10. Try it now

That is the whole stack. DNS, SMTP, parser, Postgres, polling, cleanup. Open source, free, no signup.

Keep reading