Files
webmail/lib/__tests__/server-keywords.test.ts
T
Linus Rath d7004b3851 fix: report refused tags and keep Stalwart 1.0's attachment keywords intact
Stalwart 1.0 puts `$hasattachment` or `$hasnoattachment` on every
message and derives `hasAttachment` from them alone. It also refuses
keywords over 128 bytes (0.16 cuts them to 128 characters) and more than
100 keywords on one message. The keyword writers ignored Email/set
`notUpdated`, so a refused tag showed until the next reload and then
vanished.

- `markAsRead`, `batchMarkAsRead`, `toggleStar`, `updateEmailKeywords`,
  `setKeyword`, `removeKeyword` and `batchUpdateKeywords` check the
  Email/set outcome, and the errors carry the JMAP type. Tagging from
  the list or the viewer shows "Tagging failed"; a refused category-tab
  move rejects the plugin's promise and skips the categorize hooks.
- Tag ids are capped at 121 characters (128 bytes with `$label:`).
- The attachment keywords are hidden in view source, the plugin
  projection and the `getKeywords` fallback labels, reserved for
  category tabs, and refused by the plugin keyword API. Email/copy and
  the cross-server import still carry them over.
- A tag comes off by leaving its key out of the full keyword map instead
  of being sent as false, which RFC 8621 does not allow;
  `updateEmailKeywords` also drops the false entries local updates leave
  behind (`$seen: false` after marking unread).
- The automatic mark-as-read no longer leaves an unhandled rejection
  when the server refuses it.
2026-09-25 19:24:30 +02:00

67 lines
2.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type { Email } from '@/lib/jmap/types';
import { generateEmailSource } from '@/lib/email-source';
import { emailToReadView } from '@/lib/plugin-projection';
import { isServerSetKeyword } from '@/lib/jmap/server-keywords';
// Stalwart 1.0 puts `$hasattachment` or `$hasnoattachment` on every message and
// derives hasAttachment from it. Where keywords are listed they would only
// repeat hasAttachment, so they are left out; 0.16 never sets them.
function makeEmail(keywords: Record<string, boolean>, hasAttachment: boolean): Email {
return {
id: 'm1',
threadId: 't1',
mailboxIds: { inbox: true },
keywords,
size: 2048,
receivedAt: '2026-09-25T10:00:00Z',
from: [{ name: 'Ann', email: 'ann@example.org' }],
to: [{ name: '', email: 'bob@example.org' }],
subject: 'Report',
preview: 'See attached',
hasAttachment,
textBody: [{ partId: '1', blobId: 'b1', size: 12, type: 'text/plain' }],
htmlBody: [],
bodyValues: { '1': { value: 'See attached', isEncodingProblem: false, isTruncated: false } },
} as unknown as Email;
}
describe('isServerSetKeyword', () => {
it('matches the two attachment keywords in any case, and nothing else', () => {
expect(isServerSetKeyword('$hasattachment')).toBe(true);
expect(isServerSetKeyword('$hasnoattachment')).toBe(true);
expect(isServerSetKeyword('$HasAttachment')).toBe(true);
expect(isServerSetKeyword('$seen')).toBe(false);
expect(isServerSetKeyword('$label:hasattachment')).toBe(false);
});
});
describe('server-set keywords in listings', () => {
it('leaves them out of the view-source keyword line on Stalwart 1.0', () => {
const source = generateEmailSource(makeEmail({ $seen: true, '$label:work': true, $hasattachment: true }, true));
expect(source).toContain('Has Attachment: Yes\n');
expect(source).toContain('Keywords: $seen, $label:work\n');
expect(source).not.toContain('$hasattachment');
});
it('drops the keyword line when the attachment keyword is the only one on Stalwart 1.0', () => {
const source = generateEmailSource(makeEmail({ $hasnoattachment: true }, false));
expect(source).toContain('Has Attachment: No\n');
expect(source).not.toContain('Keywords:');
});
it('keeps the plugin projection keywords to what the user set, with hasAttachment intact', () => {
const view = emailToReadView(makeEmail({ $seen: true, $hasattachment: true, $flagged: false }, true));
expect(view.keywords).toEqual(['$seen']);
expect(view.hasAttachment).toBe(true);
expect(view.source).not.toContain('$hasattachment');
});
it('lists keywords unchanged on Stalwart 0.16, which never sets them', () => {
const email = makeEmail({ $seen: true, '$label:work': true }, true);
expect(emailToReadView(email).keywords).toEqual(['$seen', '$label:work']);
expect(generateEmailSource(email)).toContain('Keywords: $seen, $label:work\n');
});
});