Files
webmail/lib/draft-source.ts
T
Linus Rath 5f0669eb92 fix: stop reopened drafts from deleting another login's mail
A reopened draft is saved over and removed by id through the login and
account it is stamped with, and message ids repeat across servers. Two
ways of opening a draft stamped it with a login that was only on
screen, not the one it was read through:

- A Pro email tab opened by double-click named no login. It read the
  message through whatever login the main list was browsing and stamped
  the active one: with the 0.16.23 login active and the 1.0 login's
  Drafts browsed, the first autosave destroyed the draft's id in the
  0.16.23 account and left the draft itself behind. A Pro view now
  stamps only a login its host names, and reads the message through
  that login's client. A double-click on a row of the mail list or of a
  folder tab names the row's login and account, and the search preview
  and a folder tab's reading pane name theirs for the active login
  too. Without a login the composer goes by the draft's From, as it did
  before.
- "Cancel and edit" in Scheduled took the login browsed before, which
  stays set while Scheduled lists the active login's mail. The draft
  was restored through the active login and then saved over and
  discarded through the other one. It now takes the active login, and
  a send undone or cancelled for editing never falls back to what is on
  screen.

Also:

- A draft version saved through another login is removed only once the
  message is out. A host without a connection sent nothing and the
  version was removed anyway; it now says so and the draft stays.
- An email tab opened for one login's message no longer focuses the tab
  of another login's message with the same id and account.
2026-09-26 04:32:25 +02:00

97 lines
4.0 KiB
TypeScript

import type { Email, Identity, Mailbox } from '@/lib/jmap/types';
import { findDraftIdentityId } from '@/lib/reply-identity';
import { stripCrossAccountIdentityPrefix } from '@/hooks/use-pro-multi-account-identities';
/**
* Where a draft opened for editing lives: the connected account (login) that
* reaches it, and the JMAP account there when it is not that login's own. The
* composer removes the old version there when it saves, sends or discards the
* draft. Email and account ids repeat across accounts and servers, so any
* other login or account may hold an unrelated message under the same id.
*/
export interface DraftSource {
localAccountId?: string;
accountId?: string;
}
/**
* The DraftSource of `draft`, a message of the list on screen:
* - `source` as it is when the caller knows it: a send undone or a scheduled
* send cancelled for editing is back in the account it was sent from, put
* there through the login `source` names (none: the composing one). What is
* on screen says nothing about it: Scheduled lists the active login's mail
* while a login browsed before still counts as viewed;
* - in a list that mixes accounts (the unified views, a tag view, an "All
* folders" search) the login and account the message is stamped with. The
* folder still selected underneath a tag view or a search says nothing
* about it;
* - otherwise the list's: the viewed login (Pro multi-account sidebar) or the
* active one, and for a shared or group folder the folder's account.
*/
export function resolveDraftSource(
draft: Pick<Email, 'sourceClientAccountId' | 'sourceAccountId'>,
context: {
source?: DraftSource;
aggregate: boolean;
mailbox?: Pick<Mailbox, 'isShared' | 'accountId'>;
viewingAccountId?: string | null;
activeAccountId?: string | null;
},
): DraftSource {
if (context.source) {
return { localAccountId: context.source.localAccountId, accountId: context.source.accountId };
}
const mailbox = context.aggregate ? undefined : context.mailbox;
return {
localAccountId: draft.sourceClientAccountId
?? context.viewingAccountId
?? context.activeAccountId
?? undefined,
accountId: draft.sourceAccountId
?? (mailbox?.isShared ? mailbox.accountId : undefined),
};
}
/**
* Where a row of the mail list was listed from, for a view that reads it
* again on its own (a Pro email tab): the login and account the row is
* stamped with in a list that mixes accounts, else the login browsed - the
* active one when no other is - and `list.accountId`, the account the list
* shows (a shared or group folder's). Nothing for a row of Scheduled: that
* list is the active login's, whatever login was browsed before.
*/
export function listRowSource(
row: Pick<Email, 'sourceClientAccountId' | 'sourceAccountId'>,
list: {
isScheduledView: boolean;
viewingAccountId?: string | null;
activeAccountId?: string | null;
accountId?: string;
},
): DraftSource {
if (row.sourceClientAccountId && row.sourceAccountId) {
return { localAccountId: row.sourceClientAccountId, accountId: row.sourceAccountId };
}
if (list.isScheduledView) return {};
return { localAccountId: list.viewingAccountId ?? list.activeAccountId ?? undefined, accountId: list.accountId };
}
/**
* The identity to reopen a draft with (findDraftIdentityId), taken from the
* identities of the login the draft lives in first: two logins can share an
* address - the same user on two servers - and the other one's identity
* would move the draft to its server on the first save.
*/
export function findDraftIdentityIdForLogin(
identities: Identity[],
from: { email?: string | null; name?: string | null } | null | undefined,
localAccountId: string | undefined,
): string | null {
if (localAccountId) {
const ofLogin = identities.filter((identity) => stripCrossAccountIdentityPrefix(identity.id).localAccountId === localAccountId);
const match = findDraftIdentityId(ofLogin, from);
if (match) return match;
}
return findDraftIdentityId(identities, from);
}