Merge pull request 'Show an assigned shared mailbox as shared, not as a locked account' (#48) from feat/shared-mailbox-kind into main
This commit was merged in pull request #48.
This commit is contained in:
commit
9f53759462
7 files changed
+51
-14
No files matched your search
+1
-1
@@ -314,7 +314,7 @@ function AuthedApp() {
|
||||
// inbuxa AL-7: a locked account in view is named, with a padlock, in the
|
||||
// tab; a shared or group mailbox (MA-A) is named without one
|
||||
const viewingName = useSession((s) => (s.viewing ? s.session?.accounts[s.viewing]?.name : undefined));
|
||||
const lockedInView = useViewingDelegation() !== null;
|
||||
const lockedInView = useViewingDelegation()?.kind === "lock";
|
||||
useEffect(() => {
|
||||
setBaseTitle(viewingName ? `${lockedInView ? "🔒 " : ""}${viewingName} · ${appName}` : appName);
|
||||
setUnreadBadge(inboxUnread);
|
||||
|
||||
@@ -13,6 +13,12 @@ export type DelegationAccess = "read" | "organize" | "full";
|
||||
|
||||
export interface Delegation {
|
||||
locked: boolean;
|
||||
/**
|
||||
* A locked account, or a shared mailbox such as support@ (MA-S). Both are
|
||||
* reached as a delegate at an access level; only how they are shown
|
||||
* differs. Absent from older servers, where it is always a lock.
|
||||
*/
|
||||
kind: "lock" | "sharedMailbox";
|
||||
access: DelegationAccess;
|
||||
sendAs: boolean;
|
||||
/** UTC date the delegation ends, if it does. */
|
||||
@@ -38,19 +44,20 @@ export function delegationOf(session: SessionLike | null, accountId: Id | null):
|
||||
const access: DelegationAccess = raw.access === "organize" || raw.access === "full" ? raw.access : "read";
|
||||
return {
|
||||
locked: true,
|
||||
kind: raw.kind === "sharedMailbox" ? "sharedMailbox" : "lock",
|
||||
access,
|
||||
sendAs: raw.sendAs === true && access !== "read",
|
||||
until: typeof raw.until === "string" ? raw.until : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Every locked account handed to the reader, by name. */
|
||||
/** Every locked account handed to the reader, by name. Shared mailboxes are listed by lib/sharedMail. */
|
||||
export function delegatedAccounts(session: SessionLike | null): DelegatedAccount[] {
|
||||
if (!session) return [];
|
||||
return Object.entries(session.accounts)
|
||||
.map(([id, account]) => {
|
||||
const delegation = delegationOf(session, id);
|
||||
return delegation ? { id, name: account.name, delegation } : null;
|
||||
return delegation?.kind === "lock" ? { id, name: account.name, delegation } : null;
|
||||
})
|
||||
.filter((a): a is DelegatedAccount => a !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Mail other people let the reader into (multi-account spec, MA-A): a group's
|
||||
* mailbox, or folders someone shared.
|
||||
* mailbox, folders someone shared, or a shared mailbox an administrator
|
||||
* assigned them to (MA-S).
|
||||
*
|
||||
* Either one arrives as another account in the session, `isPersonal: false`.
|
||||
* That alone proves nothing about mail -- the server advertises every
|
||||
@@ -9,8 +10,10 @@
|
||||
* `Mailbox/get` answers with at least one mailbox has mail the reader can
|
||||
* open, and only those are offered.
|
||||
*
|
||||
* A locked account handed to the reader (AL-7) is listed by `delegation.ts`
|
||||
* instead, and left out here so it is never offered twice.
|
||||
* A shared mailbox needs no asking: the server marks it, as a delegation of
|
||||
* kind `sharedMailbox`, and it is mail by definition. A locked account handed
|
||||
* to the reader (AL-7) is listed by `delegation.ts` instead, and left out
|
||||
* here so it is never offered twice.
|
||||
*/
|
||||
|
||||
import { CAP, client } from "@/jmap/client";
|
||||
@@ -28,7 +31,7 @@ type SessionLike = Pick<JmapSession, "accounts">;
|
||||
export function sharedMailCandidates(session: SessionLike | null): SharedMailAccount[] {
|
||||
if (!session) return [];
|
||||
return Object.entries(session.accounts)
|
||||
.filter(([id, account]) => account.isPersonal === false && CAP.mail in (account.accountCapabilities ?? {}) && !delegationOf(session, id))
|
||||
.filter(([id, account]) => account.isPersonal === false && CAP.mail in (account.accountCapabilities ?? {}) && delegationOf(session, id)?.kind !== "lock")
|
||||
.map(([id, account]) => ({ id, name: account.name }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
@@ -38,7 +41,9 @@ export async function findSharedMail(session: SessionLike | null): Promise<Share
|
||||
const candidates = sharedMailCandidates(session);
|
||||
const answers = await Promise.all(
|
||||
candidates.map((account) =>
|
||||
client.call<GetResponse<Mailbox>>("Mailbox/get", { accountId: account.id, ids: null, properties: ["id"] }).then(
|
||||
delegationOf(session, account.id)?.kind === "sharedMailbox"
|
||||
? Promise.resolve(account)
|
||||
: client.call<GetResponse<Mailbox>>("Mailbox/get", { accountId: account.id, ids: null, properties: ["id"] }).then(
|
||||
(res) => (res.list.length > 0 ? account : null),
|
||||
() => null,
|
||||
),
|
||||
|
||||
@@ -70,7 +70,7 @@ afterEach(() => {
|
||||
describe("delegation", () => {
|
||||
it("is read only from the session's mark, never from a shared account's capabilities", () => {
|
||||
const session = sessionWith(delegated("organize", true));
|
||||
expect(delegationOf(session, "locked")).toEqual({ locked: true, access: "organize", sendAs: true, until: null });
|
||||
expect(delegationOf(session, "locked")).toEqual({ locked: true, kind: "lock", access: "organize", sendAs: true, until: null });
|
||||
expect(delegationOf(session, "shared")).toBeNull();
|
||||
expect(delegationOf(session, "own")).toBeNull();
|
||||
expect(delegatedAccounts(session).map((a) => a.id)).toEqual(["locked"]);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { useSession, viewingDelegation } from "@/store/session";
|
||||
import { findSharedMail, sharedMailCandidates } from "@/lib/sharedMail";
|
||||
import { delegatedAccounts } from "@/lib/delegation";
|
||||
|
||||
/**
|
||||
* MA-A: a group's mailbox, or folders someone shared, can be opened in place
|
||||
@@ -36,6 +37,16 @@ const sessionWith = (extra: Record<string, unknown> = {}) =>
|
||||
isPersonal: false,
|
||||
accountCapabilities: { [CAP.mail]: {}, "urn:inbuxa:jmap": { delegation: { locked: true, access: "read", sendAs: false, until: null } } },
|
||||
},
|
||||
// A shared mailbox an administrator assigned (MA-S): marked, so never asked about
|
||||
desk: {
|
||||
name: "[email protected]",
|
||||
isPersonal: false,
|
||||
accountCapabilities: {
|
||||
[CAP.mail]: {},
|
||||
[CAP.calendars]: {},
|
||||
"urn:inbuxa:jmap": { delegation: { locked: true, kind: "sharedMailbox", access: "organize", sendAs: true, until: null } },
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
},
|
||||
primaryAccounts: { [CAP.mail]: "own", [CAP.calendars]: "own", [CAP.contacts]: "own", [CAP.filenode]: "own" },
|
||||
@@ -76,11 +87,12 @@ afterEach(() => {
|
||||
|
||||
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"]);
|
||||
expect(sharedMailCandidates(sessionWith()).map((a) => a.id)).toEqual(["calendarOnly", "desk", "group"]);
|
||||
});
|
||||
|
||||
it("offers only accounts that answer with a mailbox", async () => {
|
||||
expect((await findSharedMail(sessionWith())).map((a) => a.name)).toEqual(["[email protected]"]);
|
||||
// The shared mailbox answers no Mailbox/get here, and is offered anyway
|
||||
expect((await findSharedMail(sessionWith())).map((a) => a.name)).toEqual(["[email protected]", "[email protected]"]);
|
||||
});
|
||||
|
||||
it("can't be opened until it is found, and then shows mail only", async () => {
|
||||
@@ -111,4 +123,14 @@ describe("shared mail", () => {
|
||||
expect(useSession.getState().viewing).toBeNull();
|
||||
expect(useSession.getState().delegationEnded).toBe("[email protected]");
|
||||
});
|
||||
|
||||
it("shows an assigned shared mailbox as shared, not locked, keeping its access level", async () => {
|
||||
await useSession.getState().loadSharedMail();
|
||||
expect(delegatedAccounts(useSession.getState().session).map((a) => a.id)).toEqual(["locked"]);
|
||||
useSession.getState().view("desk");
|
||||
expect(useSession.getState().viewing).toBe("desk");
|
||||
expect(viewingDelegation()?.access).toBe("organize");
|
||||
// Mail only, like any shared mailbox
|
||||
expect(useSession.getState().viewAccountFor(CAP.calendars)).toBe("own");
|
||||
});
|
||||
});
|
||||
@@ -188,8 +188,8 @@ export const useSession = create<SessionState>((set, get) => ({
|
||||
viewAccountFor(cap) {
|
||||
const { session, viewing } = get();
|
||||
const viewed = viewing ? session?.accounts[viewing] : undefined;
|
||||
// A shared or group mailbox in view is mail only (MA-A)
|
||||
if (viewing && viewed && delegationOf(session, viewing) && cap in (viewed.accountCapabilities ?? {})) return viewing;
|
||||
// A shared or group mailbox in view is mail only (MA-A, MA-S)
|
||||
if (viewing && viewed && delegationOf(session, viewing)?.kind === "lock" && cap in (viewed.accountCapabilities ?? {})) return viewing;
|
||||
return ownAccountForCapability(session, cap);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -38,6 +38,9 @@ export function DelegatedBar() {
|
||||
<Users size={15} aria-hidden />
|
||||
<span className="grow truncate">
|
||||
{t("Shared mailbox:")} <strong className="notranslate" translate="no">{shared.name}</strong>
|
||||
{/* MA-S: one an administrator assigned says at what level */}
|
||||
{delegation ? ` · ${accessText(delegation.access)}` : ""}
|
||||
{delegation?.sendAs ? ` · ${t("You can send as this account")}` : ""}
|
||||
</span>
|
||||
<button type="button" onClick={back}>
|
||||
{t("Back to my mail")}
|
||||
|
||||
Reference in new issue
Block a user