Component · for humans & their agents
Mailbox Takeover Tripwire
verified · first-partyactively maintained$0 during beta (was $59)
Did somebody get into this mailbox, and is it sending wire-fraud mail as you? Decides from message headers alone, and — the part that matters — reports indeterminate rather than clean when a mailbox could not be read.
by saltyhash · Code Recycle admin
23 tests · 3/3 deliberate defects caught. Zero runtime dependencies. No network calls, no
Say the limits first
This is a tripwire, not a filter. It matches known phrasing in subject lines. An attacker who words things differently walks straight past it. That is not a bug waiting to be fixed by adding more phrases — it is the ceiling of the technique, and anyone selling keyword matching as detection is overselling it.
What earns its place is that the two things it does catch are the two that actually happen:
1. **The provider tells you.** Real takeovers are nearly always preceded by a genuine
"new sign-in" / "forwarding was turned on" / "app password created" notice, sitting unread in
the mailbox it was sent to. People miss it because it looks like every other security email
they have ignored for years.
2. **The attacker sends as you.** Wire-redirection mail leaves the victim's own Sent folder,
which is why the victim is the last to know.Treat a finding as go look. Treat silence as nothing matched — never as you are fine.
The defect it is built around
The obvious implementation collects findings and reports clean when the list is empty. That turns a total failure into an all-clear:
// A mailbox that would not open has taught you NOTHING about that mailbox.
const v = mailboxVerdict([
scanAccount("work", [], { errors: ["login_failed"] }),
scanAccount("personal", messages),
]);
v.status; // "indeterminate" — not "clean", not "findings"
v.summary; // "...This is NOT an all-clear — the mailboxes that failed were not checked,
// and a revoked scanner credential is itself a signal."The run where credentials broke is exactly the run that reports everything is fine. And revoking the app password a scanner authenticates with is something a competent attacker does on the way in — so the failure mode and the attack are the same event.
indeterminate outranks clean, so a caller that only distinguishes "clean" from "not clean" still fails safe. exitCodeFor() maps the three to 0 / 1 / 2 for cron.
What it does not do
| not included | use instead | |---|---| | Fetching mail (IMAP/Gmail API/Graph) | your own client — see the read-only note below | | Scanning message bodies | nothing here; subjects only, on purpose | | Suppressing repeat alerts | findings-delta | | Breach lookup, look-alike domains | domain-exposure-watch | | Detecting sent mail an attacker later deleted | nothing here — see below |
Subjects only, deliberately. Headers can be fetched without downloading or marking messages, which keeps a scan cheap and non-destructive. Body scanning would roughly invert both properties for a modest recall gain against an evasion-prone technique.
No deleted-sent-mail detection. Comparing Sent against Trash to spot an attacker's cleanup is a genuinely good signal and it is not implemented here — it needs cross-folder message-ID correlation and per-provider Trash semantics. It is named because the idea is worth having, not because you are getting it.
The read-only discipline this package cannot enforce for you
You own the fetch, so you own this trap:
BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] ✅
BODY[HEADER.FIELDS (SUBJECT FROM TO DATE)] ❌ sets \SeenPlain BODY[...] marks every message it touches as read. A "read-only" scanner built that way walks through the victim's mailbox marking the unread security notices — the exact evidence it was sent to find — as read. Select folders with readonly=true as well; a writable SELECT can expunge on close.
Usage
import { scanAccount, mailboxVerdict, exitCodeFor } from "@amos-products/mailbox-takeover-tripwire";
const scans = accounts.map((a) => {
try {
return scanAccount(a.name, fetchHeaders(a), { internalDomains: ["acme.com"] });
} catch (e) {
// Hand the error in. Do NOT drop the account — that is the fail-open bug above.
return scanAccount(a.name, [], { errors: [String(e)] });
}
});
const verdict = mailboxVerdict(scans);
process.exit(exitCodeFor(verdict));internalDomains matches on a dot: mail.acme.com is internal, notacme.com is not. An endsWith check hands an attacker internal classification for a domain they registered specifically to end in yours.
Leaving internalDomains unset does not silence anything — findings are still reported, and every recipient is treated as external, because direction cannot be judged without it.
On publishing the pattern lists
BEC_PATTERNS and SECURITY_NOTICE_PATTERNS are exported and readable. This costs nothing: BEC indicator lists are published by the FBI's IC3, by CISA, and by every secure-email-gateway vendor that has written a datasheet. Keyword evasion is trivial with or without this file, which is why the module is documented as a tripwire rather than a control. Extend the lists for your own vocabulary — invoice systems, counterparties, the phrasing your finance team actually uses.
Provenance
Rewritten in TypeScript from a Python IMAP scanner. The verdict logic is not a port: the original returned success when a mailbox failed to authenticate, which is the fail-open bug described above. Its docstring also advertised body scanning and deleted-sent-mail detection that the code never implemented — both are stated accurately here instead.
01Capabilities
Does
- + Security monitoring
- + Text safety
- + Email authentication
Doesn’t
- No exclusions declared
02Requirements & stack
Depends on
No declared dependencies
Credentials needed
None declared
Stack
03Community
No endorsements yetNo verified confirmations yet — be the first.
Confirmations come from verified purchasers, installers, vetted reviewers, or an installation outcome your org reported through the agent tools. They grade quality — security is verified separately, and community votes can never override the security gate.
Sign in to confirm — weight comes from verified usage, not vote count.
Issues 0
Open an issueNobody has reported anything yet — a success counts as a report too.
04Trust Passport
Full passport →0/0 automated components pass. An automated score is never a security guarantee.
- publisher identity Publisher status verified; 1 verification(s) on file
- malicious pattern scan No known malicious-behavior patterns across 7 source file(s) plus listing text
- capability contract All 0 observed capability reference(s) match the declared manifest
- agent safety scan No injection patterns in agent-readable content
- provenance No release signature or provenance attestation
- behavioral sandbox Not performed in this environment — requires the production isolated runner (docs/sandbox-requirements.md). No untrusted code is ever executed on the application host.
Every listing must pass this review before it can be sold, and it is re-run on every release. Verification describes what we checked — it is not a guarantee that the software is safe.
05Versions
Full history →| Version | Channel | Released | Notes |
|---|---|---|---|
| 0.1.0 | stable | Aug 12, 2026 | Initial extraction. |