Set the Archive role from ihasmail, rather than describing it
#220 corrected the message and left it useless: it told you a folder needs the Archive role on the server, which was true, and gave you nothing to do about it here. Roles were shown in Folders settings and never settable. Mailbox/set takes `role`. Confirmed live against 0.16.20 on 2026-09-02, as an ordinary user through the proxy, with no admin API: setting role "archive" on a folder that had none returned updated and the folder began working as the Archive immediately. Stalwart parses the role names in SpecialUse::parse, "archive" among them, refuses a second holder of a role, and refuses to move the role of Inbox, Junk or Trash. So the toast now carries the fix. "No Archive folder is set yet." with a Create one that makes the folder and then completes the archiving that could not happen -- rather than leaving someone to select the same messages again. A folder already named Archive and carrying no role is adopted rather than duplicated. That is the state #217 was reported from, and a second Archive beside the first would be its own confusion. One named Archive that is really the Sent folder is left alone: taking its role to fix archiving would break sending. Folders settings gains a Role column. Archive, Drafts and Sent are offered, being the roles this client's behaviour depends on and the server will move; Inbox, Junk and Trash show theirs and cannot change it, because 0.16.20 refuses. A role another folder holds is left out of the list rather than offered and refused, so freeing it is a deliberate two steps. The folder is created with the server's own name, never the localised one, for the reason renaming already writes back the server's: a German session must not create "Archiv" that an English one cannot find. Closes #217 properly.
This commit is contained in:
1 parent
8e9259638d
commit
5e6e049eef
13 files changed
+350
-23
No files matched your search
+66
-5
@@ -178,7 +178,9 @@ export interface MailState {
|
||||
/** The mailbox plus all of its descendants. */
|
||||
descendantMailboxIds(mailboxId: Id): Id[];
|
||||
|
||||
createMailbox(name: string, parentId: Id | null): Promise<Id>;
|
||||
createMailbox(name: string, parentId: Id | null, role?: MailboxRole): Promise<Id>;
|
||||
/** Give something the Archive role -- adopting a folder already named for it, or making one. */
|
||||
ensureArchiveFolder(): Promise<Id>;
|
||||
updateMailbox(id: Id, patch: Partial<Mailbox>): Promise<void>;
|
||||
destroyMailbox(id: Id, removeEmails?: boolean): Promise<void>;
|
||||
|
||||
@@ -213,6 +215,34 @@ function listKey(q: { filter: EmailFilter; sort: Comparator[]; collapseThreads:
|
||||
|
||||
export const DEFAULT_SORT: Comparator[] = [{ property: "receivedAt", isAscending: false }];
|
||||
|
||||
/**
|
||||
* Nothing carries the Archive role, so offer to fix it rather than explain it.
|
||||
*
|
||||
* The message this replaces described the problem accurately and left the
|
||||
* reader with nothing to do inside ihasmail -- roles were only ever shown, not
|
||||
* set. `Mailbox/set` takes `role`, so the offer is real: one click makes the
|
||||
* folder and files the messages that were being archived when it was missing.
|
||||
*
|
||||
* `retry` is the archiving that could not happen, handed back so the click
|
||||
* finishes the job rather than leaving someone to select the same messages
|
||||
* again.
|
||||
*/
|
||||
function offerArchiveFolder(retry: () => Promise<void>): void {
|
||||
toast.error(t("No Archive folder is set yet."), {
|
||||
action: {
|
||||
label: t("Create one"),
|
||||
onClick: async () => {
|
||||
try {
|
||||
await useMail.getState().ensureArchiveFolder();
|
||||
await retry();
|
||||
} catch (err) {
|
||||
toast.error(t("Could not set up an Archive folder: {error}", { error: (err as Error).message }));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const useMail = create<MailState>((set, get) => ({
|
||||
accountId: null,
|
||||
mailboxes: {},
|
||||
@@ -610,7 +640,7 @@ export const useMail = create<MailState>((set, get) => ({
|
||||
async archive(ids) {
|
||||
const archiveId = get().roleId("archive") ?? get().roleId("all");
|
||||
if (!archiveId) {
|
||||
toast.error(t("No Archive folder is set. A folder needs the Archive role on the server; naming it “Archive” is not enough."));
|
||||
offerArchiveFolder(() => get().archive(ids));
|
||||
return;
|
||||
}
|
||||
await get().move(ids, archiveId, { label: "Archive" });
|
||||
@@ -621,7 +651,7 @@ export const useMail = create<MailState>((set, get) => ({
|
||||
const archiveId = get().roleId("archive") ?? get().roleId("all");
|
||||
if (!accountId || !ids.length) return;
|
||||
if (!archiveId) {
|
||||
toast.error(t("No Archive folder is set. A folder needs the Archive role on the server; naming it “Archive” is not enough."));
|
||||
offerArchiveFolder(() => get().archiveByDate(ids, granularity));
|
||||
return;
|
||||
}
|
||||
const { emails } = get();
|
||||
@@ -809,15 +839,46 @@ export const useMail = create<MailState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
async createMailbox(name, parentId) {
|
||||
async createMailbox(name, parentId, role) {
|
||||
const accountId = get().accountId!;
|
||||
const res = await client.call<SetResponse<Mailbox>>("Mailbox/set", { accountId, create: { n: { name, parentId, isSubscribed: true } } });
|
||||
const n: Record<string, unknown> = { name, parentId, isSubscribed: true };
|
||||
// Only when asked. Sending `role: null` on every create would be harmless
|
||||
// and would still say something the caller did not.
|
||||
if (role) n.role = role;
|
||||
const res = await client.call<SetResponse<Mailbox>>("Mailbox/set", { accountId, create: { n } });
|
||||
const err = res.notCreated?.n;
|
||||
if (err) throw new Error(setErrorMessage(err));
|
||||
await get().loadMailboxes();
|
||||
return res.created!.n!.id;
|
||||
},
|
||||
|
||||
/*
|
||||
* The Archive folder, made rather than described.
|
||||
*
|
||||
* `Mailbox/set` takes `role` -- confirmed live against 0.16.20 on 2026-09-02,
|
||||
* as an ordinary user through the proxy, no admin API -- so a missing Archive
|
||||
* is something ihasmail can fix instead of explaining a server-side concept
|
||||
* and leaving. Stalwart parses the role names in `SpecialUse::parse`, of
|
||||
* which "archive" is one, and enforces that a role is held by one folder.
|
||||
*
|
||||
* A folder already *named* Archive but carrying no role is adopted rather
|
||||
* than duplicated. That is exactly the state #217 was reported from -- a
|
||||
* folder with the right name and no role, which archiving could not see --
|
||||
* and creating a second Archive beside it would be its own confusion.
|
||||
*
|
||||
* The name is the server's, not a translated one, for the same reason
|
||||
* renaming writes back the server's own: a folder's name is data, and a
|
||||
* German session must not create "Archiv" that an English one cannot find.
|
||||
*/
|
||||
async ensureArchiveFolder() {
|
||||
const existing = Object.values(get().mailboxes).find((m) => !m.role && m.name.trim().toLowerCase() === "archive");
|
||||
if (existing) {
|
||||
await get().updateMailbox(existing.id, { role: "archive" });
|
||||
return existing.id;
|
||||
}
|
||||
return get().createMailbox("Archive", null, "archive");
|
||||
},
|
||||
|
||||
async updateMailbox(id, patch) {
|
||||
const accountId = get().accountId!;
|
||||
// Paths as the filter rules currently spell them, before the move.
|
||||
|
||||
Reference in new issue
Block a user