This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
It took my laptop 129 seconds to read the envelopes of 7,556 emails. Then it gave me a list of 199 accounts it thinks I made. Four were hacked after I joined. 33 may still have my card. One hadn't written to me in 7.8 years.
To check its work, I went through 200 senders by hand, one at a time. The page had three buttons: "Yes, I signed up", "No", and "Can't remember, skip it". I pressed the third one 34 times.
Every account you make stays out there after you forget it: the quiz site from school, the app you tried for a week, the shop you bought one thing from.
So I built Haunted Inbox. It finds them in your own mail, on your own computer, and gives you a plain report: what was hacked, what may have your card, and where to go to delete it. One click away, the same accounts stand as a graveyard you can walk through, one stone for each, on the day you signed up.
The test I wrote for it said it finds 93% of accounts. My real mail said 64%. That gap is the most useful thing in this post, and it's further down.
Here it is in 90 seconds, sound on:
The inbox in the opening is an animation of made-up mail, and the graveyard is the public sample. The report near the end is my real one, with the names blurred.
TL;DR
What: a local tool that finds the accounts in your mailbox. Each one shows when you joined, whether the place was hacked after that, who owns it now, and its last words to you: the last subject line it ever sent. How: Gemma 4 E2B, running on my laptop through Ollama, reads one short card per sender and answers one question: is this an account? Plain code decides everything you're told as a fact. The result: on 166 senders from my real mail that I marked by hand, a keyword search found 65 of 159 accounts and Gemma found 101, with 3 false alarms each. Where it fails: it still lets 3 of 6 test scams through, and web search once sold me the wrong company called Notion. Both are below. Try it with nothing to install: haunted.celebrable.tech. MIT licensed, on GitHub.
The report comes first: the riskiest account at the top, and folder tabs for what it found.
Haunted Inbox answers one question: who still has me? It's for anyone who has kept the same email address for a few years. The theme asked for something spooky, or something that honours the past. An old account is a bit of both: a small piece of who you used to be, still holding your name.
You drop in a Google Takeout file or use an app password, and it reads only the envelope of each email: who sent it, when, and the subject.
What you get looks like a page out of a burial register, with ruled paper, typed entries and rubber stamps. Folder tabs along the top say what it found: hacked after you joined, may still have your card, silent for years.
Open an account and its file slides out of the cabinet:
A made-up person's Evernote. The dates, the last message and the new owner are all in the file.
| In the file | Where it comes from |
|---|---|
| When you joined, how long it's been silent | The first and last email in your mailbox |
| How we know you have an account | One of your own subject lines, chosen by code |
| Its last words to you | The last subject line it ever sent |
| Hacked after you joined | The public Have I Been Pwned list, only if the hack came after your first email |
| May still have your card | A receipt, invoice or renewal in your subject lines |
| Who owns it now | Live web search, on request, with the source linked |
| How to leave | The public JustDeleteMe list, or their own help page |
The strangest line in the file is the last words. On the sample, the oldest account is Neopets, and it signs off with "We miss you, Sam". Ask what became of the others and the report says: "12 now belong to someone else. Nobody asked you."
Leaving takes two steps. Close the account on their own site, then mark it closed here. A CLOSED stamp lands on the file, and you can undo it.
The graveyard is one click away. Press "Walk through the graveyard" and you go left to right through the years of your life online, with a small hooded keeper beside you. Its lantern swings a moment late, because it hangs. Hold the button on a stone: candles light, the ghost squashes down and spirals away, the stone sinks, and flowers grow where it stood.
Every ghost you settle makes the sky a little lighter. The stars go, the sun comes up over the hill the moon is setting behind, and the bare trees get leaves. Clear the graveyard and it's morning.
Why not just search your inbox for "welcome"? That's the usual advice, and it's my baseline below. It can't see an account whose mail doesn't use the word, or one that writes in another language.
Others have looked for old accounts in email. Find My Accounts reads an export locally, Paperweight is open source, and Consumer Reports warns that some services ask for your email password. I haven't tested them. What I wanted was a small open model reading the mail, a test anyone can rerun, and something you'd actually want to look at.
Live: haunted.celebrable.tech, with nothing to install. It replays a real scan of a made-up person's mailbox: 3,951 emails over fourteen years, built from real company names. Every hack shown really happened, and every "sold to" came from a real web search.
A few things to try:
You forgot them. They didn't forget you.
Also on YouTube. The inbox in the opening is an animation of made-up mail, and the graveyard is the public sample. The report near the end is a real one, with the names blurred.
Every account your past self made is still out there, holding your name, your email, sometimes your card. Haunted Inbox finds them from your own mail, on your own computer, and lays them out as a graveyard you can walk through: one stone for every account, on the day you signed up, with a ghost above it.
You get a plain report first: what it found, the riskiest account, and a link to where you can delete each one. The graveyard is the same accounts, one click away: each stone tells you what that account still knows, whether it…
MIT licensed. It's about 3,000 lines with the tests, the Python needs nothing outside the standard library, and the page has no frameworks. To run it on your own mail, you need Python 3.12 and Ollama:
git clone https://github.com/nirajkhetre/haunted-inbox
cd haunted-inbox
ollama pull gemma4:e2b
python -m haunted serve
Then drop in a Google Takeout file, or use an app password with Gmail, Yahoo, iCloud, Fastmail, AOL or Zoho.
your mailbox ─▶ envelopes only: from, date, subject
─▶ code: one short card per sender
─▶ Gemma 4 E2B (Ollama, JSON schema): {kind, service}
─▶ code: joined date, proof, last words, hack check, delete link
─▶ the report and the graveyard
A mailbox is the worst place to let a model make things up, so I gave it the smallest job I could. Code turns the mailbox into one card per sender, like this made-up one:
Sender: Rappi <rappi.com>
Emails: 33, 2017-03-04 to 2022-06-01
Subjects:
1. Bienvenido a Rappi
2. Tu pedido va en camino
3. Tu recibo de hoy
Gemma answers with two fields: kind (account, list, person or other) and service (the company's plain name). Everything else is code:
.xyz with fewer than 10 emails is never a ghost, whatever the wording says.BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE LIST-UNSUBSCRIBE LIST-ID)], so the server never sends the message, and the connection is read-only.
I wrote 102 senders by hand and froze them in git (ml/frozen.py). There are 56 accounts, including 14 whose mail has no keyword at all ("Someone liked your review", "Your year in film") and 22 in other languages. The other 46 are 22 newsletters, 10 of them with words like "password" and "order" in their headlines, 12 people, and 12 others, 6 of which are scams.
| Reader | Accounts found | False alarms | No keyword | Other languages | Per card |
|---|---|---|---|---|---|
| Searching for "welcome", "verify", "password" | 13 / 56 | 15 / 46 | 0 / 14 | 0 / 22 | instant |
| Gemma, first prompt | 49 / 56 | 7 / 46 | 7 / 14 | 22 / 22 | 0.32 s |
| Gemma, current prompt | 52 / 56 | 5 / 46 | 12 / 14 | 20 / 22 | 0.26 s |
The last row is flattered: I changed the prompt after seeing the middle row's mistakes, on this same test. It also got worse at other languages, 22 down to 20, in exchange for finding far more accounts with no keywords. And I wrote this test to be hard on keywords, which is why the next section matters more.
You can rerun it: python ml/evaluate.py trick gemma4:e2b.
Both tests so far are ones I wrote. So I scanned my own mail, then marked 200 senders by hand on a page that doesn't show what the model said (check.html). Skipping the 34 I couldn't remember left 166. These are totals only, and none of it left my laptop.
| My inbox | |
|---|---|
| Emails read (envelopes only) | 7,556 |
| Different senders | 311 |
| Time, start to finish | 129 seconds |
| Ghosts | 199 |
| Hacked after I joined | 4 |
| May still have my card | 33 |
| Silent for over a year | 57 |
| Silent the longest | 7.8 years |
| Reader, on my 166 marked senders | Accounts found | False alarms |
|---|---|---|
| Searching for keywords | 65 / 159 | 3 / 7 |
| Gemma | 101 / 159 | 3 / 7 |
Gemma found 55% more accounts than the keyword search. Where the two disagreed, Gemma was right 45 times and the keywords 9 (McNemar p < 0.001).
It also did far worse than my test said it would. It found 93% of the accounts on the frozen test and 64% on my real mail. I wrote that test in the style I expected mail to be in, and real mail isn't.
Of the 58 accounts it missed, it called 53 a newsletter. My marking page asked "Yes, I signed up", and I had, so they count against it. You could argue a newsletter isn't really an account, but it still holds your email, so I left the score as it is.
Only 7 senders were marked "no", so "3 false alarms" says very little either way. I also tried counting a sender if either reader said yes, which finds 109 with 4 false alarms. I didn't ship it, because on the frozen test keyword matches were false alarms 15 times out of 46.
And half of my ghosts aren't dead. The median one last wrote 0.4 years ago, so the list is closer to everything I've signed up for than everything I've forgotten. I kept them all, because "I still use this one" is one press, and hiding an account you've forgotten is worse than showing one you remember.
"What became of this company?" uses SerpApi for live Google results, and it only ever sends the company's public name. You agreed to terms with a company that may not own your data any more, so I wanted the card to say who does. It took three tries.
Try one: let Gemma read the results and copy out a sentence. It came back empty for Tumblr, even though the results plainly said Automattic bought it.
Try two: code finds the sentences, and Gemma says yes or no. This found things, and also told me:
| What the sentence said | What the card would have said |
|---|---|
| "Dropbox acquired Reclaim.ai for $40.2M" | Dropbox was sold to Reclaim.ai |
| "Grammarly has announced its acquisition of ... Superhuman" | Grammarly was sold to Superhuman |
| "Robinhood has acquired ... Cove Markets" | Robinhood was sold to Cove Markets |
| "Zynga has shut down, pulled from the ..." | Zynga shut down |
A small model reads "acquired" and says yes. It doesn't reliably notice who did it to whom.
Try three: code checks the wording runs the right way round, and only then does Gemma confirm it. On the sample's 44 ghosts, that found 13 sales, and all four wrong answers above were gone.
I read all 13, and 12 look right, including Microsoft and LinkedIn, Bending Spoons and Evernote, Reddit and Dubsmash. One was simply wrong: "Jama Software Acquires Notion" is about a different Notion. No wording rule can catch that, so the card says "found by web search, so check it", and I took that one out of the demo by hand. It also misses sales it should find, like Neopets. All my tries together cost 124 searches.
Every delete link was broken, and every check passed. I wrote this:
const safe = u => /^https?:///i.test(u || '') ? esc(u) : '#';
JavaScript reads /^https?:/ as the whole pattern and everything after // as a comment. So safe() returned a regular expression instead of a link, every "Go and delete it" pointed nowhere, and node --check was perfectly happy. One backslash per slash fixed it.
Gemma was right, and my code threw it away. On the sample, Gemma seemed to miss 12 of the 46 accounts. When I printed what it had said, it had called 10 of those 12 an account. I had also asked for the number of the subject that proved it, and it often answered 0, or twice the number of emails (77 and 83). My "no proof, no ghost" rule rejected them all. Now code picks the proof, and that same sample finds 44 of 46 with no false alarms.
The biggest mailboxes would have crashed. Python's imaplib refuses any line over 1 MB, and IMAP returns every message number in a mailbox as one line. So I raised the limit. I can't test that one, because my mailbox isn't big enough.
The first design was a cartoon: a bubble font, a big mailbox character, ghosts everywhere, and a button that said "Post this letter". It looked good. Then I imagined arriving with a real need, worried about an old account, and I thought: is this a game?
So I stopped and read. Google's research on expressive design, 46 studies and more than 18,000 people, says "emotion can't make up for a lack of clarity", and warns that expressive styling may not suit sensitive contexts. Nielsen Norman Group found trust explained about 52% of whether people would recommend a site, and friendliness about 8%.
| It looked like a game because | Now |
|---|---|
| A clever headline that said nothing | "Find every account you forgot you made." The spooky line comes second |
| A metaphor on the main button | "See a sample report". The letter still flies when you press it |
| A cartoon world as the only view | A plain report first. The graveyard is a choice |
| Nothing on what it costs or reads | Free, no account, what it reads, results, and what it misses, on the page |
| Jokes next to real risks | Plain words, a table, and undo |
Then the fixes themselves looked generated: a popular serif, and rounded dark cards with number tiles. So I went back to the project's own world. A cemetery keeps a burial register, which is a table, so it's serious and it's themed. The headings are in Piazzolla, whose letters look cut with a chisel, entries are typed in Courier Prime, and the buttons are shaped like the brass plates on old stones.
The theatre is now spent on a few moments: the letter going in, the graveyard, the ritual, the sunrise. Sound is off until you ask for it, and it's played by instruments built in the browser. The public site is about 380 KB, with no frameworks or libraries.
Because of what's in a subject line. Who you bank with, what you bought, which clinic you went to, which apps you tried at 2 AM. Across my 311 senders, that's my life online since 2018, and it's the one thing I would never paste into someone else's API.
Because it's how I got the true number. The 64% only exists because the model ran on my laptop. If this had needed a hosted API, I wouldn't have sent it my real mail. I'd have stopped at the frozen test, printed 93%, and believed it.
Because a scan costs $0 and nobody can take it away. Gemma 4 E2B is a 4.6 GB download that reads a card in about a quarter of a second. There's no key and no account, and it keeps working if a company changes its prices or goes away, which is fitting for a tool about companies that went away.
Because the lists are open too. Have I Been Pwned's public list gave me 953 verified breaches, and JustDeleteMe, an MIT-licensed list kept up by volunteers, covers 2,685 services. Both ship inside the repo, so nothing gets looked up about you. And you can read every line that touches your mail.
Where a closed model would be better: I can't give you a number, because I didn't test one. A big hosted model might well score higher on my frozen test and catch more of those newsletters. I can't run the comparison that matters, because that test is my mail.
On Día de los Muertos you set out an ofrenda for the people you've lost, you tell their stories, and there are marigolds. I wanted leaving an old account to feel closer to that than to a security chore. These accounts are old versions of you: the kid on Neopets, the person who was going to learn Spanish. You walk past them with a lantern, say goodbye to the ones that are done, and keep the ones you still use. Then it gets light.
The moment I can show you is that marking page. It was 200 senders, one at a time, and 34 times I pressed "Can't remember, skip it". Each of those 34 has my email address, and I can't tell you what else I gave them. That's what this tool is for.
Breach list: Have I Been Pwned (CC BY 4.0). Delete links: JustDeleteMe (MIT). Model: Gemma, run with Ollama. Live search: SerpApi. The film's voice is from Fish Audio, which isn't open source; its music and sound effects come from the page's own instruments. Fonts: Piazzolla, Atkinson Hyperlegible Next and Courier Prime (SIL Open Font License).
The repository was started on 31 October 2026, inside the challenge window.
If you run it on your own mail, tell me in the comments how long your quietest ghost has been silent. Mine: 7.8 years.