mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: offer filters and auto-reply only for shared accounts that support them (bulwarkmail/webmail#1239)
This commit is contained in:
@@ -43,11 +43,28 @@ beforeEach(() => {
|
||||
accounts: {
|
||||
own: { name: 'me', isPersonal: true, isReadOnly: false, accountCapabilities: { [CAPABILITIES.SIEVE]: SIEVE_CAPS } },
|
||||
team: { name: 'Team', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
crew: { name: 'Crew', isPersonal: false, isReadOnly: false },
|
||||
other: { name: 'Other', isPersonal: true, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountSupportsSieve on a shared account that lists its capabilities (#1239)', () => {
|
||||
it('is false when the list leaves this one out', () => {
|
||||
const caps = { [CAPABILITIES.SIEVE]: {}, [CAPABILITIES.MAIL]: {} };
|
||||
expect(accountSupportsSieve(
|
||||
{ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
caps,
|
||||
)).toBe(false);
|
||||
expect(accountSupportsSieve(
|
||||
{ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.SIEVE]: {} } },
|
||||
caps,
|
||||
)).toBe(true);
|
||||
// An empty list says nothing: the server-wide answer stands.
|
||||
expect(accountSupportsSieve({ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: {} }, caps)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountSupportsSieve', () => {
|
||||
const caps = { [CAPABILITIES.SIEVE]: {} };
|
||||
|
||||
@@ -76,7 +93,9 @@ describe('accountSupportsSieve', () => {
|
||||
describe('isSieveSupported', () => {
|
||||
it('checks the own Sieve account by default and a named account on request', () => {
|
||||
expect(isSieveSupported()).toBe(true);
|
||||
expect(isSieveSupported('team')).toBe(true);
|
||||
// Team lists its capabilities and Sieve isn't one (#1239); Crew lists none.
|
||||
expect(isSieveSupported('team')).toBe(false);
|
||||
expect(isSieveSupported('crew')).toBe(true);
|
||||
expect(isSieveSupported('other')).toBe(false);
|
||||
expect(isSieveSupported('missing')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -30,12 +30,28 @@ beforeEach(() => {
|
||||
capabilities: { [CAPABILITIES.VACATION]: {} },
|
||||
accounts: {
|
||||
own: { name: 'me', isPersonal: true, isReadOnly: false, accountCapabilities: { [CAPABILITIES.VACATION]: {} } },
|
||||
team: { name: 'Team', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
team: { name: 'Team', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {}, [CAPABILITIES.VACATION]: {} } },
|
||||
other: { name: 'Other', isPersonal: true, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountSupportsVacation on a shared account that lists its capabilities (#1239)', () => {
|
||||
it('is false when the list leaves this one out', () => {
|
||||
const caps = { [CAPABILITIES.VACATION]: {}, [CAPABILITIES.MAIL]: {} };
|
||||
expect(accountSupportsVacation(
|
||||
{ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.MAIL]: {} } },
|
||||
caps,
|
||||
)).toBe(false);
|
||||
expect(accountSupportsVacation(
|
||||
{ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: { [CAPABILITIES.VACATION]: {} } },
|
||||
caps,
|
||||
)).toBe(true);
|
||||
// An empty list says nothing: the server-wide answer stands.
|
||||
expect(accountSupportsVacation({ name: 'grp', isPersonal: false, isReadOnly: false, accountCapabilities: {} }, caps)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountSupportsVacation', () => {
|
||||
const caps = { [CAPABILITIES.VACATION]: {} };
|
||||
|
||||
|
||||
+5
-8
@@ -2,6 +2,7 @@ import { jmapClient } from './jmap-client';
|
||||
import { CAPABILITIES, type JMAPAccountInfo } from './types';
|
||||
import { secureFetch } from '../lib/client-cert';
|
||||
import type { SieveScript, SieveCapabilities } from '../lib/sieve/types';
|
||||
import { accountSupports } from '../lib/account-capability';
|
||||
|
||||
// JMAP Sieve (RFC 9661) bindings. Mirrors the webmail JMAPClient Sieve methods
|
||||
// but follows the mobile convention of a functional api/* module driving the
|
||||
@@ -30,18 +31,14 @@ export function getSieveAccountId(): string {
|
||||
|
||||
// Gate on the ACCOUNT capability, not only the server-wide session capability:
|
||||
// RFC 9661 advertises Sieve per account, and an account without Sieve rights
|
||||
// fails every SieveScript call. Stalwart doesn't always advertise capabilities
|
||||
// on shared/group accounts, so treat non-personal accounts as capable - the
|
||||
// rule the webmail's getSharedAccounts() applies. A server that populates no
|
||||
// accountCapabilities at all keeps the session-wide answer.
|
||||
// fails every SieveScript call. A shared account that lists no capabilities
|
||||
// at all, and a server that populates none, keep the session-wide answer
|
||||
// (lib/account-capability, webmail #1239).
|
||||
export function accountSupportsSieve(
|
||||
account: JMAPAccountInfo | undefined,
|
||||
sessionCapabilities: Record<string, unknown> | undefined,
|
||||
): boolean {
|
||||
if (!sessionCapabilities || !(CAPABILITIES.SIEVE in sessionCapabilities)) return false;
|
||||
if (!account) return false;
|
||||
if (!account.isPersonal || !account.accountCapabilities) return true;
|
||||
return CAPABILITIES.SIEVE in account.accountCapabilities;
|
||||
return accountSupports(account, sessionCapabilities, CAPABILITIES.SIEVE);
|
||||
}
|
||||
|
||||
export function isSieveSupported(accountId?: string): boolean {
|
||||
|
||||
+5
-7
@@ -1,5 +1,6 @@
|
||||
import { jmapClient } from './jmap-client';
|
||||
import { CAPABILITIES, type JMAPAccountInfo } from './types';
|
||||
import { accountSupports } from '../lib/account-capability';
|
||||
|
||||
export interface VacationResponse {
|
||||
id: string;
|
||||
@@ -65,17 +66,14 @@ export async function setVacationResponse(
|
||||
|
||||
// Gate on the ACCOUNT capability: RFC 8621 advertises VacationResponse per
|
||||
// account, so the session can list it while a given account lacks it.
|
||||
// Stalwart doesn't always advertise capabilities on shared/group accounts, so
|
||||
// treat non-personal accounts as capable, as the webmail does. A server that
|
||||
// populates no accountCapabilities at all keeps the session-wide answer.
|
||||
// A shared account that lists no capabilities at all, and a server that
|
||||
// populates none, keep the session-wide answer (lib/account-capability,
|
||||
// webmail #1239).
|
||||
export function accountSupportsVacation(
|
||||
account: JMAPAccountInfo | undefined,
|
||||
sessionCapabilities: Record<string, unknown> | undefined,
|
||||
): boolean {
|
||||
if (!sessionCapabilities || !(CAPABILITIES.VACATION in sessionCapabilities)) return false;
|
||||
if (!account) return false;
|
||||
if (!account.isPersonal || !account.accountCapabilities) return true;
|
||||
return CAPABILITIES.VACATION in account.accountCapabilities;
|
||||
return accountSupports(account, sessionCapabilities, CAPABILITIES.VACATION);
|
||||
}
|
||||
|
||||
export function isVacationSupported(accountId?: string): boolean {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { JMAPAccountInfo } from '../api/types';
|
||||
|
||||
/**
|
||||
* Whether the user may use a capability's methods with an account (webmail
|
||||
* JMAPClient.accountSupports, #1239). The account's own accountCapabilities
|
||||
* decide (RFC 8620 2): a server can advertise a capability while an account
|
||||
* has the matching rights revoked. Some shared accounts list no capabilities
|
||||
* at all, so a shared account (isPersonal false) with an empty list falls
|
||||
* back to the server-wide capability, as does a server that populates no
|
||||
* accountCapabilities anywhere. A shared account that lists capabilities but
|
||||
* not this one used to count as capable.
|
||||
*/
|
||||
export function accountSupports(
|
||||
account: JMAPAccountInfo | undefined,
|
||||
sessionCapabilities: Record<string, unknown> | undefined,
|
||||
capability: string,
|
||||
): boolean {
|
||||
if (!sessionCapabilities || !(capability in sessionCapabilities)) return false;
|
||||
if (!account) return false;
|
||||
const caps = account.accountCapabilities;
|
||||
if (caps && capability in caps) return true;
|
||||
if (caps && Object.keys(caps).length > 0) return false;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user