mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
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.
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
// Projection helpers - convert host-internal types into the read-only views
|
|
// that plugins consume. Keeping this in one place ensures every slot/hook
|
|
// hands plugins the same shape declared in plugin-types.ts.
|
|
|
|
import type { Email } from '@/lib/jmap/types';
|
|
import type { EmailReadView } from '@/lib/plugin-types';
|
|
import { generateEmailSource } from '@/lib/email-source';
|
|
import { isServerSetKeyword } from '@/lib/jmap/server-keywords';
|
|
|
|
// Resolve a message's plain-text body from its JMAP body parts. Plugins that
|
|
// translate or scan content need the real body, not just the short `preview`
|
|
// snippet. Prefers text/plain parts; falls back to stripped HTML, then preview.
|
|
function plainTextFromEmail(email: Email): string {
|
|
const values = email.bodyValues || {};
|
|
const collect = (parts?: { partId: string }[]) =>
|
|
(parts || [])
|
|
.map((p) => values[p.partId]?.value)
|
|
.filter((v): v is string => typeof v === 'string' && v.length > 0)
|
|
.join('\n')
|
|
.trim();
|
|
|
|
const text = collect(email.textBody);
|
|
if (text) return text;
|
|
|
|
const html = collect(email.htmlBody);
|
|
if (html) {
|
|
return html
|
|
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ /gi, ' ')
|
|
.replace(/&/gi, '&')
|
|
.replace(/</gi, '<')
|
|
.replace(/>/gi, '>')
|
|
.replace(/'|'/gi, "'")
|
|
.replace(/"/gi, '"')
|
|
.replace(/[ \t]{2,}/g, ' ')
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
.trim();
|
|
}
|
|
|
|
return (email.preview || '').trim();
|
|
}
|
|
|
|
export function emailToReadView(email: Email): EmailReadView {
|
|
return {
|
|
id: email.id,
|
|
threadId: email.threadId,
|
|
mailboxIds: Object.keys(email.mailboxIds || {}).filter(k => email.mailboxIds[k]),
|
|
from: (email.from || []).map(a => ({ name: a.name || '', email: a.email })),
|
|
to: (email.to || []).map(a => ({ name: a.name || '', email: a.email })),
|
|
cc: (email.cc || []).map(a => ({ name: a.name || '', email: a.email })),
|
|
subject: email.subject || '',
|
|
receivedAt: email.receivedAt,
|
|
isRead: !!email.keywords?.['$seen'],
|
|
isFlagged: !!email.keywords?.['$flagged'],
|
|
hasAttachment: email.hasAttachment,
|
|
preview: email.preview || '',
|
|
text: plainTextFromEmail(email),
|
|
headers: email.headers,
|
|
source: generateEmailSource(email),
|
|
// Stalwart 1.0's attachment keywords are what `hasAttachment` says.
|
|
keywords: Object.keys(email.keywords || {}).filter(k => email.keywords[k] && !isServerSetKeyword(k)),
|
|
auth: email.authenticationResults,
|
|
};
|
|
}
|