Skip to content

fix(auth): clear the previous user's state on sign-out - #583

Merged
maathimself merged 1 commit into
maathimself:mainfrom
Monkey7539:fix/signout-residue
Oct 11, 2026
Merged

maathimself merged 1 commit into
maathimself:mainfrom
Monkey7539:fix/signout-residue

Conversation

@Monkey7539

Copy link
Copy Markdown
Collaborator

Summary

An expired session signs out without reloading the page. request() dispatches mailflow:session_expired for any 401 outside /auth/ (api.js#L27-L29), five wrong PINs on the lock screen end the session the same way (api.js#L158-L164), and App.jsx answers with setUser(null) and setLocked(false) (App.jsx#L27). When the user changes, setUser resets only the unread counts and the sender favicons (store/index.js#L110-L133). So whoever signs in next in that tab is shown the previous user's open compose, with its recipients, subject and quoted text (MailApp.jsx#L920), and their message list, search and message windows until their own data loads. Settings held only in memory stay the previous user's until loadPreferences replaces them (store/index.js#L1143), and the ones it sets only when the new user has saved them (#L1211-L1234) carry over for the whole session: remote images load if the previous user allowed them, their image whitelist, hidden folders and shortcuts apply, and their auto-lock timer can lock a user who has no PIN to unlock with. After a lockout, setLocked(false) also restores the message that was open when the previous user locked the screen (store/index.js#L181-L184).

Sign out from the sidebar and from the lock screen reload the page through the shared signOut helper (#523), so this is about the paths that do not reload. One gap on the reload path too: the two newer reading preferences, auto-opening reply drafts and what opens after a removal, are seeded from localStorage when the store is created (store/index.js#L472, #L733-L734) but are not on SIGN_OUT_CLEARED_KEYS (signOut.js#L8-L14), so the next sign-in starts from the previous user's choice.

Changes

  • store/index.js: when a signed-in user leaves, that is setUser with another id or with null, the store also resets what locking clears (messages, search, the selection, threads, accounts, folders, notifications, backfill progress, GTD sections and category counts), the compose state, message windows and reply-draft lookups, and the settings held only in memory (enabledPlugins, aiActions, blockRemoteImages, imageWhitelist, hiddenFolders, shortcuts, autoLockMinutes, categorizationEnabled, gtdPetSlug, autoOpenReplyDrafts, afterRemove). It also removes mailflow_locked_message, so the lock clear that follows a lockout has nothing to restore. A cold load goes from no user to one and resets nothing, so the two preferences seeded from localStorage still apply before loadPreferences answers. An update to the same user, as Settings does after a PIN or 2FA change (AdminPanel.jsx#L7771), resets nothing. replyDraftRevision is left alone: lookups and handoffs in flight already stop on the user id.
  • utils/signOut.js: mailflow_auto_open_reply_drafts and mailflow_after_remove join SIGN_OUT_CLEARED_KEYS. Appearance settings are still kept (theme matching login screen #208).
  • Tests. New store/signOutState.test.js drives the store the way App.jsx handles a 401, then signs in a second user: the compose left open is gone; every mailbox field and every in-memory setting in the reset is seeded with the first user's data and must equal the store's initial value from useStore.getInitialState(), so a field dropped from the reset, or a value that drifts from the default, fails by name; the message open at the lock is cleared on the lockout path; a same-user update keeps the compose and the accounts; a cold load keeps the two seeded preferences. utils/signOut.test.js gains a case for the two keys. Two existing tests asserted that a composer survives an identity change (InboxReplyDraftObserver.test.js, the ownership cases, and ComposeModal.replyDraft.test.js, "a mounted draft save response cannot update a different signed-in owner"); they now assert that it is closed, which is the point of the change.

Behaviour changes:

  • After an expired session, signing back in no longer shows the compose window from before it. What had been typed was already lost when the composer unmounted at the 401, and autosaved copies stay in Drafts.
  • After an expired session, the settings above, and whether GTD shows, sit at their defaults until loadPreferences finishes, the same as on a cold load.
  • Sign out clears the two reading preferences from localStorage, like the other mailbox settings on the list.

Not part of this PR:

  • Settings kept in localStorage (page size, sounds, conversation view and so on) and the folder state (favourite, recent and collapsed folders, expanded accounts) still carry over on the 401 path to a next user who has not saved their own, and that user's first change saves the previous user's entries into their own preferences. Resetting them in memory would not help, because they come back from localStorage on the next load, and clearing localStorage there changes what a same-user sign-in after an expiry keeps. They need their own design.
  • Per-user localStorage keys that no sign-out clears: favourite and recent folders, the selected account and folder, the default sender, the last From account, the folder-order cache, the Todoist flag and the AI results cache. Some are meant to survive a sign-in by the same user, so they need a separate look.
  • Custom CSS applied by loadPreferences stays applied after an expired-session sign-out until the page reloads.
  • After five wrong PINs the server ends the session without an end-session URL, so with RP-initiated logout on, Sign in with SSO can still get past the lock. That needs the unlock route to return the URL.

Testing

  • Against main, five of the seven new tests fail: composing is still true, the first user's account is still listed, the selected message is still m1, enabledPlugins is still ['gtd'], and mailflow_after_remove survives the sign-out. The same-user and cold-load tests pass before and after; they guard against resetting too eagerly. Without the mailflow_locked_message removal, the lockout test fails because setLocked(false) restores m1. With the reset on every identity change rather than on a leaving user, the cold-load test fails. The two adapted tests fail in their original form against the fix, three cases.
  • Frontend: npm test 2809/2809, npm run lint clean, npm run build succeeds.
  • Backend: unchanged.

Contributor License Agreement

By submitting this pull request I confirm that:

  • I have read and agree to the Contributor License Agreement.
  • My contribution is my own original work (or I have identified any
    third-party material and confirmed it is compatible with the CLA).
  • I have the right to submit this contribution under the terms of the CLA.

🤖 Generated with Claude Code

An expired session signs out without reloading the page: App.jsx answers
a 401 with setUser(null), and setUser reset only the unread counts and
sender favicons when the user changed. Whoever signed in next in that
tab was shown the previous user's open compose, with its recipients,
subject and quoted text, and their message list, search and message
windows until their own data loaded. Settings held only in memory stayed
the previous user's until loadPreferences replaced them, and the ones it
sets only when the new user has saved them (remote images, the image
whitelist, hidden folders, shortcuts, the auto-lock timer) carried over
for the whole session. After a lockout, setLocked(false) also restored
the message that was open when the previous user locked the screen.

When a signed-in user leaves, setUser now resets the mailbox state that
locking clears, the compose and message windows, the reply-draft
lookups and the settings held only in memory, and drops the message
remembered for the lock screen. A cold load goes from no user to one and
resets nothing, so the reading preferences the store seeds from
localStorage still apply before loadPreferences answers. Signing out
also clears the localStorage keys of the auto-open reply draft and
after-remove preferences, which the next sign-in otherwise started from.
Two tests that had a composer survive an identity change now assert that
it is closed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@maathimself
maathimself merged commit c318c3f into maathimself:main Oct 11, 2026
2 checks passed
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.

2 participants