Open a group's mailbox, or folders someone shared, from the account menu
ci / version (pull_request) Skipped
ci / github (pull_request) Skipped
ci / node (pull_request) Successful in 1m29s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 41s
github/ci (branch) GitHub Actions
ci / version (pull_request) Skipped
ci / github (pull_request) Skipped
ci / node (pull_request) Successful in 1m29s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 41s
github/ci (branch) GitHub Actions
A group's members, and anyone a folder was shared with, could reach that mail over IMAP but not here: Mail read only the reader's own account. Calendar, Contacts and Files already list other people's shares; Mail now does too (multi-account spec, MA-A). Such an account is listed under "Mail to show" in the account menu, after any locked account handed to the reader, and opens in place of the reader's own mail through the same switch AL-7 uses. Only accounts whose Mailbox/get answers with a mailbox are offered: the server advertises every capability on any shared account, so a colleague who shared one calendar would otherwise appear with mail. While one is in view: - a bar in the palette's accent (not the locked account's red) names it, with "Back to my mail"; the tab title names it without a padlock; - calendars, contacts and files stay the reader's own (viewAccountFor follows only a delegation now); - writing a message uses the viewed account's identities, so a reply in support@ goes out as support@ and is saved in its Drafts and Sent. Losing the account (removed from the group, share withdrawn) takes the reader back to their own mail with the existing "You no longer have access" notice. New string: "Shared mailbox:" (1, English only in the other ten catalogs). Checked in Chrome against a scratch server with a support@ group and two members. typecheck and vitest (174 files, 1533 tests) pass.
This commit is contained in:
1 parent
e94508ea89
commit
722a68432c
7 files changed
+244
-28
No files matched your search
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CAP, client } from "@/jmap/client";
|
||||
import type { JmapSession } from "@/jmap/types";
|
||||
import { useSession } from "@/store/session";
|
||||
import { findSharedMail, sharedMailCandidates } from "@/lib/sharedMail";
|
||||
|
||||
/**
|
||||
* MA-A: a group's mailbox, or folders someone shared, can be opened in place
|
||||
* of the reader's own mail. Only accounts that answer with a mailbox are
|
||||
* offered, only mail follows the switch, and losing the account takes the
|
||||
* reader back.
|
||||
*/
|
||||
|
||||
const sessionWith = (extra: Record<string, unknown> = {}) =>
|
||||
({
|
||||
capabilities: { [CAP.core]: { maxCallsInRequest: 16, maxObjectsInGet: 500 }, [CAP.mail]: {} },
|
||||
accounts: {
|
||||
own: {
|
||||
name: "[email protected]",
|
||||
isPersonal: true,
|
||||
accountCapabilities: { [CAP.mail]: {}, [CAP.calendars]: {}, [CAP.contacts]: {}, [CAP.filenode]: {} },
|
||||
},
|
||||
// A group: every capability, and mailboxes
|
||||
group: {
|
||||
name: "[email protected]",
|
||||
isPersonal: false,
|
||||
accountCapabilities: { [CAP.mail]: {}, [CAP.calendars]: {}, [CAP.contacts]: {}, [CAP.filenode]: {} },
|
||||
},
|
||||
// Someone who shared one calendar: mail is advertised, but no mailbox answers
|
||||
calendarOnly: { name: "[email protected]", isPersonal: false, accountCapabilities: { [CAP.mail]: {}, [CAP.calendars]: {} } },
|
||||
// No mail at all
|
||||
filesOnly: { name: "[email protected]", isPersonal: false, accountCapabilities: { [CAP.filenode]: {} } },
|
||||
// A locked account handed over: listed by delegation.ts, never here
|
||||
locked: {
|
||||
name: "[email protected]",
|
||||
isPersonal: false,
|
||||
accountCapabilities: { [CAP.mail]: {}, "urn:inbuxa:jmap": { delegation: { locked: true, access: "read", sendAs: false, until: null } } },
|
||||
},
|
||||
...extra,
|
||||
},
|
||||
primaryAccounts: { [CAP.mail]: "own", [CAP.calendars]: "own", [CAP.contacts]: "own", [CAP.filenode]: "own" },
|
||||
state: "s",
|
||||
}) as unknown as JmapSession;
|
||||
|
||||
/** Accounts whose Mailbox/get answers with a mailbox. */
|
||||
let withMailboxes = new Set(["group"]);
|
||||
let nextSession: JmapSession;
|
||||
|
||||
beforeEach(() => {
|
||||
withMailboxes = new Set(["group"]);
|
||||
nextSession = sessionWith();
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string, init?: RequestInit) => {
|
||||
if (String(url).includes("/api/auth/session")) {
|
||||
return { ok: true, status: 200, json: async () => nextSession } as Response;
|
||||
}
|
||||
const { methodCalls } = JSON.parse((init?.body as string) ?? "{}") as { methodCalls: [string, Record<string, unknown>, string][] };
|
||||
const methodResponses = methodCalls.map(([name, args, id]) => [
|
||||
name,
|
||||
{ accountId: args.accountId, state: "1", list: withMailboxes.has(String(args.accountId)) ? [{ id: "a" }] : [], notFound: [] },
|
||||
id,
|
||||
]);
|
||||
return { ok: true, status: 200, json: async () => ({ methodResponses, sessionState: "s" }) } as Response;
|
||||
}),
|
||||
);
|
||||
const session = sessionWith();
|
||||
client.session = session;
|
||||
useSession.setState({ status: "authenticated", session, accountId: "own", viewing: null, sharedMail: [], delegationEnded: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useSession.setState({ viewing: null, sharedMail: [] });
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("shared mail", () => {
|
||||
it("considers only other people's accounts that advertise mail, leaving locked accounts to delegation", () => {
|
||||
expect(sharedMailCandidates(sessionWith()).map((a) => a.id)).toEqual(["calendarOnly", "group"]);
|
||||
});
|
||||
|
||||
it("offers only accounts that answer with a mailbox", async () => {
|
||||
expect((await findSharedMail(sessionWith())).map((a) => a.name)).toEqual(["[email protected]"]);
|
||||
});
|
||||
|
||||
it("can't be opened until it is found, and then shows mail only", async () => {
|
||||
useSession.getState().view("group");
|
||||
expect(useSession.getState().viewing).toBeNull();
|
||||
|
||||
await useSession.getState().loadSharedMail();
|
||||
useSession.getState().view("group");
|
||||
expect(useSession.getState().viewing).toBe("group");
|
||||
// Calendars, contacts and files stay the reader's own
|
||||
expect(useSession.getState().viewAccountFor(CAP.calendars)).toBe("own");
|
||||
expect(useSession.getState().viewAccountFor(CAP.contacts)).toBe("own");
|
||||
expect(useSession.getState().viewAccountFor(CAP.filenode)).toBe("own");
|
||||
|
||||
// An account with no mailboxes is never offered
|
||||
useSession.getState().view("calendarOnly");
|
||||
expect(useSession.getState().viewing).toBe("group");
|
||||
});
|
||||
|
||||
it("takes the reader back to their own mail when the account goes away", async () => {
|
||||
await useSession.getState().loadSharedMail();
|
||||
useSession.getState().view("group");
|
||||
const session = sessionWith();
|
||||
delete (session.accounts as Record<string, unknown>).group;
|
||||
nextSession = session;
|
||||
withMailboxes = new Set();
|
||||
await useSession.getState().refresh();
|
||||
expect(useSession.getState().viewing).toBeNull();
|
||||
expect(useSession.getState().delegationEnded).toBe("[email protected]");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user