Skip to content

feat: report phishing to the organisation's address, as in the webmail #124 - #133

Closed
wmcparla wants to merge 1 commit into
bulwarkmail:mainfrom
wmcparla:feat/report-phishing
Closed

wmcparla wants to merge 1 commit into
bulwarkmail:mainfrom
wmcparla:feat/report-phishing

Conversation

@wmcparla

@wmcparla wmcparla commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The app gets the webmail's "Report phishing" (#124). While the organisation policy sets organisation.phishingReport, the message's More menu and the list's selection bar offer Report phishing…. After a confirm that names the address, each message goes to it as its own report, and is then filed in Junk with $phishing, with Undo. A reported message has its links turned off.

It ports the webmail's lib/phishing-report.ts, lib/phishing-report-flow.ts, lib/phishing-report-arf.ts, lib/phishing-links.ts and stores/phishing-report-store.ts, and follows them unless noted below.

Changes

  • Policy: phishingReportSetup() reads organisation.phishingReport from the policy the app already fetches (usePolicyStore().effective), and checks the address again because policy.json is written by hand.
  • Which messages: the same rule as the webmail. Not the user's own Sent, Drafts or Scheduled mail (by the open folder, and by the folders a search hit is in, looked up in its own account), not a draft, and not one already reported.
  • Sending (src/api/phishing-report.ts): one report per message, from the login's own address.
    • Forward: the original is attached as message/rfc822 by its blob id, copied with Blob/copy first when it is in a shared or group mailbox.
    • ARF (RFC 5965): the original's bytes are downloaded from its own account, wrapped, uploaded and submitted raw. A message too big for maxSizeUpload is refused before the download.
    • sendEmail gains noSentCopy (onSuccessDestroyEmail instead of the move to Sent), so no copy stays in Sent or Drafts. A copy the server doesn't destroy is reported as a filing warning.
  • Filing: a new store action fileReported files each reported message in its own account's Junk, by account and id, through the existing fileAcrossAccounts. That gives one Undo for all, the offline queue and the cache updates as for Report spam.
    • It works by account and id rather than by the open list because a selection's reports take a while, and by then the user may have opened another folder, perhaps another account's, where a different message has the same id.
    • It sets $junk, $phishing and $seen. Undo puts back each message's own keywords and folder, and shows the webmail's "Moved back. The report was already sent."
    • api/markAsSpam takes further keywords for $phishing. Plain Report spam is unchanged.
  • Order, as in the webmail:
    • A message counts as reported (usePhishingReportStore) as soon as its own report went out, so its links go off and it isn't offered again even if the move then fails.
    • A message whose report failed stays where it is.
    • Nothing starts offline, since the report can't be queued.
    • If another login is opened while a selection's reports are going out, the rest are left as failed.
  • Links off: for a message with $phishing, or one reported in this session:
    • its body's links lose their address (turnOffLinks in email-html.ts, through the tokenizer the sanitiser already uses), and the WebView refuses every navigation;
    • it shows the webmail's banner;
    • the unsubscribe banner and the structured-mail cards are hidden, since they are the sender's;
    • an .eml or HTML attachment previewed from it has its links off too.
  • Strings: the webmail's phishing_report block, copied into each vendored locales/<lang>/common.json as the webmail has it (translated in every language, nothing machine-made). A later sync-locales writes the same text.
  • Icon: Fish, as the webmail uses, added through scripts/generate-icons.mjs (Tabler 3.50.0).

Related issues

Closes #124

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactor / code quality improvement
  • Chore / dependency update / CI change

Checklist

  • I have read the Contributing Guide
  • My code follows the project's code style and conventions
  • I have run npm run typecheck and there are no errors (native has no lint script)
  • The build passes (no npm run build in native; an x86_64 release APK, assembleGithubRelease -PallowDebugSignedRelease=true, builds and runs)
  • I have tested my changes locally
  • I have added or updated documentation if needed (none needed)
  • I have updated translations (locales/) if my changes affect user-facing text (the webmail's own translations; npm run i18n:check passes)
  • I have included screenshots or a screen recording for UI changes

Tests: npx vitest run: 7143 passed, 163 skipped (the live Stalwart suites), 1 failed. The failure is postal-mime-rn.test.ts, which fails the same on main in my checkout ("Creating blobs from 'ArrayBuffer' … are not supported"). The new tests cover:

  • the policy reading;
  • which messages can be reported;
  • the forward and ARF requests: attachment by blob, Blob/copy, onSuccessDestroyEmail, the upload limit, a refused submission;
  • the ARF parts, ported with the webmail's tests, plus header injection;
  • turnOffLinks on HTML and plain-text bodies;
  • fileReported: its own account's Junk while another account's folder is open, list removal, Undo;
  • the flow: a failed report stays put, offline, partial moves, Undo unmarking only what came back, a login switch mid-way.

Tested on the Android 16 Pixel 9 emulator against a self-hosted Stalwart 1.1.0, with two test accounts:

Each case was predicted before the run. One account reports and the other is the report address; messages were seeded over JMAP and the server state read back over JMAP, not judged from the screen.

# Case Result
1 Viewer: More > Report phishing…, confirm One report received, its message/rfc822 part byte-equal to the original; no copy in the reporter's Sent or Drafts; original in Junk with $junk, $phishing, $seen
2 Open it again from Junk Banner shown; tapping the link opens nothing; no Report phishing… in More
3 Report, then Undo "Moved back. The report was already sent."; back in Inbox, $junk/$phishing cleared, $seen as it was; one report; offered again
4 Cancel in the confirm (control) Nothing sent, nothing changed
5 List: select 3, Report phishing… Three separate reports, one attachment each; all three in Junk with $phishing; one Undo bar
6 A message in Sent Not offered, in the viewer or the selection bar
7 Airplane mode "The message couldn't be reported." with its subject; nothing sent; message stays in Inbox
8 ARF format multipart/report; report-type=feedback-report, Feedback-Type: fraud, original byte for byte; no copy kept
9 Plain Report spam (regression) Junk with $junk only, nothing sent

Not tested: iOS (no Mac here). A real organisation policy on the device: the test accounts sign in without a webmail, so for the emulator build the address came from a local stand-in, and the policy reading is covered by unit tests only. Accounts signed in with a password get the webmail's public policy, which leaves out organisation, so for them the action stays hidden, as for the app's other organisation rules.

Screenshots / demo

Pixel 9 emulator. The boxes cover the test accounts' mail domain.

Confirm, naming the address Undo bar after the report Opened again from Junk
The Report phishing? alert: The message goes to (the report address) and moves to Junk, with Cancel and Report The Inbox with the undo bar: Reported to (the report address) and moved to Junk, Undo A message reported as phishing: the banner 'You reported this message as phishing. Its links are turned off.' above a body whose link is shown as dotted text

In the third, the link is drawn as plain dotted text and opens nothing.

Notes for reviewers

  • Not in this PR: reporting from a unified view's selection (rows from several logins); and a domain or group scope with its own report address for a shared mailbox (the login's own policy applies, where the webmail resolves it per account).
  • Same flow for spam reports: Mark as spam: report to the inbound gateway's spam address webmail#1591 proposes it for "Mark as spam" with a filtering gateway's address. If it lands, sendPhishingReport and fileReported would serve it here.
  • No in-app sheet for the confirm: it is the system alert the app already uses for its other confirmations, rather than a copy of the webmail's dialog.

🤖 Generated with Claude Code

…bulwarkmail#124

Port of the webmail's Report phishing (lib/phishing-report*.ts,
lib/phishing-links.ts). While the organisation policy sets
organisation.phishingReport, the message's More menu and the list's
selection bar offer "Report phishing…": after a confirm naming the
address, each message goes to it as its own report (forwarded with the
original attached as message/rfc822 by its blob, or as an ARF report,
as the admin chose), with no copy kept. Each one reported is then filed
in its own account's Junk with $junk, $phishing and $seen, with Undo.
A message whose report fails stays where it is; nothing starts offline.

A message carrying $phishing (or reported in this session) has its
links turned off, with the webmail's banner, and shows no unsubscribe
banner or structured-mail cards; files previewed from it have their
links off too. The strings are the webmail's phishing_report block, in
every locale.
@wmcparla
wmcparla marked this pull request as ready for review November 13, 2026 19:45
@bulwark-warden

Copy link
Copy Markdown

I found nothing to flag in the send path.

  • No copy in Sent: the new noSentCopy option uses onSuccessDestroyEmail instead of the move to Sent. The webmail sends its forward report with the same option (webmail lib/phishing-report.ts).
  • Forward format, own account: copyBlobs makes no request when the message is in the login's own account, so it adds nothing there (src/api/email.ts).

The CI test job hadn't finished when I looked.

Bulwark Warden is an AI assistant and can be wrong. It answers from the docs, the code and earlier issues, and links its sources.

@rathlinus

Copy link
Copy Markdown
Member

Thanks for the thorough port. Report phishing landed on main in 70b0d65 and e3a47be through the catch-up with the webmail before I got to this, so I'm closing this one as superseded. I took two of your additions into deca0bf: a reported message no longer shows its unsubscribe banner or structured-mail cards, and an .eml or HTML attachment previewed from it has its links off. Reporting several messages from the list's selection isn't on main yet; a separate PR on top of main's version is welcome.

@rathlinus rathlinus closed this Nov 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Report phishing, as in the webmail

2 participants