fix: standardize punctuation

This commit is contained in:
Linus Rath
2026-04-16 19:07:42 +02:00
parent 6b57118add
commit 8bdadc7ba3
107 changed files with 452 additions and 448 deletions
+8 -8
View File
@@ -69,7 +69,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api Mailbox/get', () => {
describe('POST /api - Mailbox/get', () => {
it('should return list of mailboxes', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -88,7 +88,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api Email/query', () => {
describe('POST /api - Email/query', () => {
it('should filter by mailbox', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -117,7 +117,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api Email/get', () => {
describe('POST /api - Email/get', () => {
it('should return emails by ids', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -150,7 +150,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api Email/set', () => {
describe('POST /api - Email/set', () => {
it('should update email keywords', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -166,7 +166,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api Identity/get', () => {
describe('POST /api - Identity/get', () => {
it('should return identities', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -183,7 +183,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api unknown method', () => {
describe('POST /api - unknown method', () => {
it('should return error for unknown methods', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -199,7 +199,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api back-references', () => {
describe('POST /api - back-references', () => {
it('should resolve #ids from Email/query result', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -220,7 +220,7 @@ describe('dev-jmap mock server', () => {
});
});
describe('POST /api invalid request', () => {
describe('POST /api - invalid request', () => {
it('should return 400 for missing methodCalls', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
+6 -6
View File
@@ -68,7 +68,7 @@ describe('JMAPClient resilience', () => {
return client;
}
describe('authenticatedFetch network error retry', () => {
describe('authenticatedFetch - network error retry', () => {
it('retries once on transient network error', async () => {
const client = await createConnectedClient();
const echoResponse = { methodResponses: [['Core/echo', { ping: 'pong' }, '0']] };
@@ -95,7 +95,7 @@ describe('JMAPClient resilience', () => {
});
});
describe('authenticatedFetch basic auth 401 session refresh', () => {
describe('authenticatedFetch - basic auth 401 session refresh', () => {
it('refreshes session and retries on 401 for API requests', async () => {
const client = await createConnectedClient();
const refreshedSession = makeSession({ apiUrl: 'https://mail.example.com/jmap/api-v2' });
@@ -138,12 +138,12 @@ describe('JMAPClient resilience', () => {
// connect() should throw without trying to refresh session (would cause infinite recursion)
await expect(client.connect()).rejects.toThrow('Invalid username or password');
// Only one fetch call no refresh attempt
// Only one fetch call - no refresh attempt
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
});
describe('authenticatedFetch bearer token refresh', () => {
describe('authenticatedFetch - bearer token refresh', () => {
it('refreshes token and retries on 401 for bearer mode', async () => {
const tokenRefresh = vi.fn().mockResolvedValue('new-token-456');
const session = makeSession();
@@ -172,7 +172,7 @@ describe('JMAPClient resilience', () => {
});
});
describe('authenticatedFetch 429 rate limiting', () => {
describe('authenticatedFetch - 429 rate limiting', () => {
it('stops sending authenticated requests until the retry window expires', async () => {
const client = await createConnectedClient();
@@ -258,7 +258,7 @@ describe('JMAPClient resilience', () => {
// So ping throws, keep-alive catches it, fires false
// Then reconnect → connect() → authenticatedFetch(sessionUrl) succeeds
fetchSpy
// ping fails network error, retry also fails
// ping fails - network error, retry also fails
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
// reconnect → connect() → session URL succeeds
+9 -9
View File
@@ -267,13 +267,13 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
it('should keep nested folders when a subfolder has the same name as a role mailbox', () => {
// Reporter's exact scenario: two subfolders with the same name.
// The dedup uses substring matching and removes non-role folders whose name
// matches a role folder even if they're deep in the tree with children.
// matches a role folder - even if they're deep in the tree with children.
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'sent-role', name: 'Sent', role: 'sent' }),
// User-created subfolder also named "Sent" nested under Inbox
makeMailbox({ id: 'sent-custom', name: 'Sent', parentId: 'inbox' }),
// Child of the custom "Sent" folder becomes orphaned if parent is deduped
// Child of the custom "Sent" folder - becomes orphaned if parent is deduped
makeMailbox({ id: 'sent-child', name: 'Archive', parentId: 'sent-custom' }),
];
@@ -281,7 +281,7 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
const flat = flattenMailboxTree(tree);
const rootIds = tree.map(n => n.id);
// sent-custom MUST be kept because it has children removing it orphans sent-child
// sent-custom MUST be kept because it has children - removing it orphans sent-child
const sentCustom = flat.find(n => n.id === 'sent-custom');
expect(sentCustom).toBeDefined();
expect(sentCustom!.depth).toBe(1); // nested under Inbox
@@ -293,7 +293,7 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
});
it('should keep nested folders when name is substring of a role name', () => {
// "Draft" is a substring of "Drafts" dedup removes it, orphaning children
// "Draft" is a substring of "Drafts" - dedup removes it, orphaning children
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'drafts-role', name: 'Drafts', role: 'drafts' }),
@@ -343,22 +343,22 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
it('should only dedup root-level non-role mailboxes that duplicate role mailboxes', () => {
// Dedup should only remove mailboxes that are BOTH:
// 1. At root level (no parentId) same structural position as role mailbox
// 1. At root level (no parentId) - same structural position as role mailbox
// 2. Name-matching a role mailbox
// Nested mailboxes with matching names should always be kept.
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'sent-role', name: 'Sent', role: 'sent' }),
makeMailbox({ id: 'sent-dup', name: 'Sent Mail' }), // root-level duplicate OK to remove
makeMailbox({ id: 'sent-dup', name: 'Sent Mail' }), // root-level duplicate - OK to remove
makeMailbox({ id: 'proj', name: 'Projects', parentId: 'inbox' }),
makeMailbox({ id: 'sent-nested', name: 'Sent', parentId: 'proj' }), // nested must keep
makeMailbox({ id: 'sent-nested', name: 'Sent', parentId: 'proj' }), // nested - must keep
makeMailbox({ id: 'report', name: 'Report', parentId: 'sent-nested' }),
];
const tree = buildMailboxTree(mailboxes);
const flat = flattenMailboxTree(tree);
// "Sent Mail" at root (no parentId) can be deduped that's fine
// "Sent Mail" at root (no parentId) can be deduped - that's fine
// But "Sent" nested under Projects must be kept
const sentNested = flat.find(n => n.id === 'sent-nested');
expect(sentNested).toBeDefined();
@@ -377,7 +377,7 @@ describe('mailbox orphan behavior (missing parent)', () => {
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'INBOX', role: 'inbox' }),
makeMailbox({ id: 'privat', name: 'PRIVAT', parentId: 'inbox' }),
// 'bookings' is MISSING simulating truncated JMAP response
// 'bookings' is MISSING - simulating truncated JMAP response
makeMailbox({ id: 'hotel2', name: 'HOTEL2', parentId: 'bookings' }),
makeMailbox({ id: 'restaurant', name: 'RESTAURANT', parentId: 'hotel2' }),
];
+1 -1
View File
@@ -92,7 +92,7 @@ describe('oauth/discovery', () => {
expect(consoleSpy).toHaveBeenCalled();
});
it('caches results second call for same server URL does not re-fetch', async () => {
it('caches results - second call for same server URL does not re-fetch', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(VALID_METADATA),
+1 -1
View File
@@ -13,7 +13,7 @@ import { useIdentityStore } from '@/stores/identity-store';
import { useVacationStore } from '@/stores/vacation-store';
import { useSmimeStore } from '@/stores/smime-store';
// Minimal snapshot shapes we only capture what we need
// Minimal snapshot shapes - we only capture what we need
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type StoreSnapshot = Record<string, any>;
+2 -2
View File
@@ -115,7 +115,7 @@ export async function initAdminPassword(): Promise<boolean> {
}
if (isHashed(envPassword)) {
// Already hashed in env save to file
// Already hashed in env - save to file
const data: AdminData = {
passwordHash: envPassword,
createdAt: new Date().toISOString(),
@@ -129,7 +129,7 @@ export async function initAdminPassword(): Promise<boolean> {
return true;
}
// Cleartext hash it
// Cleartext - hash it
const hash = await hashPassword(envPassword);
const data: AdminData = {
passwordHash: hash,
+1 -1
View File
@@ -50,7 +50,7 @@ export function getPathPrefix(locale?: string): string {
* through unchanged.
*
* Server code (route handlers, layout files running at SSR) should keep using
* the raw Fetch API the mount prefix is a browser-only concept.
* the raw Fetch API - the mount prefix is a browser-only concept.
*
* @example
* await apiFetch('/api/jmap', { method: 'POST', body })
+1 -1
View File
@@ -6,7 +6,7 @@ import { generateDemoId } from './demo-utils';
/**
* In-memory JMAP client for demo mode.
* All data lives in memory no network calls, no cookies.
* All data lives in memory - no network calls, no cookies.
*/
export class DemoJMAPClient implements IJMAPClient {
private data: DemoData;
+3 -3
View File
@@ -7,9 +7,9 @@ export function generateDemoId(prefix: string = 'demo'): string {
/**
* Generate an ISO date string relative to "now".
* @param daysOffset whole days from today
* @param hoursOffset additional hours offset (default 0)
* @param minutesOffset additional minutes offset (default 0)
* @param daysOffset - whole days from today
* @param hoursOffset - additional hours offset (default 0)
* @param minutesOffset - additional minutes offset (default 0)
*/
export function demoDate(daysOffset: number, hoursOffset: number = 0, minutesOffset: number = 0): string {
const d = new Date();
+5 -5
View File
@@ -63,8 +63,8 @@ export function createDemoEmails(): Email[] {
textBody: [{ partId: '1', blobId: 'blob-5', size: 450, type: 'text/plain' }],
htmlBody: [{ partId: '2', blobId: 'blob-6', size: 650, type: 'text/html' }],
bodyValues: {
'1': { value: 'Hi team,\n\nI wanted to share the updated timeline for our Q4 deliverables:\n\n- Phase 1: Design review Oct 15\n- Phase 2: Development Nov 1-30\n- Phase 3: Testing Dec 1-15\n- Phase 4: Launch Dec 20\n\nPlease review and let me know if you see any conflicts.\n\nBest,\nAlice' },
'2': { value: '<p>Hi team,</p><p>I wanted to share the updated timeline for our Q4 deliverables:</p><ul><li>Phase 1: Design review Oct 15</li><li>Phase 2: Development Nov 1-30</li><li>Phase 3: Testing Dec 1-15</li><li>Phase 4: Launch Dec 20</li></ul><p>Please review and let me know if you see any conflicts.</p><p>Best,<br>Alice</p>' },
'1': { value: 'Hi team,\n\nI wanted to share the updated timeline for our Q4 deliverables:\n\n- Phase 1: Design review - Oct 15\n- Phase 2: Development - Nov 1-30\n- Phase 3: Testing - Dec 1-15\n- Phase 4: Launch - Dec 20\n\nPlease review and let me know if you see any conflicts.\n\nBest,\nAlice' },
'2': { value: '<p>Hi team,</p><p>I wanted to share the updated timeline for our Q4 deliverables:</p><ul><li>Phase 1: Design review - Oct 15</li><li>Phase 2: Development - Nov 1-30</li><li>Phase 3: Testing - Dec 1-15</li><li>Phase 4: Launch - Dec 20</li></ul><p>Please review and let me know if you see any conflicts.</p><p>Best,<br>Alice</p>' },
},
messageId: '<q4-timeline-1@example.com>',
},
@@ -83,7 +83,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-7', size: 520, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Looks good to me! One concern: the testing window might be tight given the holidays. Could we start testing a few days earlier?\n\nAlso, should we set up a shared doc for tracking blockers?\n\n Bob' },
'1': { value: 'Looks good to me! One concern: the testing window might be tight given the holidays. Could we start testing a few days earlier?\n\nAlso, should we set up a shared doc for tracking blockers?\n\n- Bob' },
},
messageId: '<q4-timeline-2@example.com>',
inReplyTo: ['<q4-timeline-1@example.com>'],
@@ -104,7 +104,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-8', size: 400, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today and share the link.\n\nUpdated timeline:\n- Design review: Oct 15\n- Development: Nov 1-27\n- Testing: Nov 28 - Dec 15\n- Launch: Dec 20\n\n Alice' },
'1': { value: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today and share the link.\n\nUpdated timeline:\n- Design review: Oct 15\n- Development: Nov 1-27\n- Testing: Nov 28 - Dec 15\n- Launch: Dec 20\n\n- Alice' },
},
messageId: '<q4-timeline-3@example.com>',
inReplyTo: ['<q4-timeline-2@example.com>'],
@@ -291,7 +291,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-17', size: 480, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hey,\n\nI\'ve been thinking about our rate limiting approach and wanted to propose:\n\n1. Token bucket algorithm instead of fixed window\n2. Per-endpoint limits rather than global\n3. Graduated response (warn → throttle → block)\n\nThoughts? I can put together a more detailed RFC if we agree on the direction.\n\n Bob' },
'1': { value: 'Hey,\n\nI\'ve been thinking about our rate limiting approach and wanted to propose:\n\n1. Token bucket algorithm instead of fixed window\n2. Per-endpoint limits rather than global\n3. Graduated response (warn → throttle → block)\n\nThoughts? I can put together a more detailed RFC if we agree on the direction.\n\n- Bob' },
},
messageId: '<project-2@example.com>',
},
+19 -19
View File
@@ -104,7 +104,7 @@ const EMAIL_LIST_PROPERTIES = [
function isTaskObject(obj: { '@type'?: string; progress?: unknown; due?: unknown; percentComplete?: unknown }): boolean {
const type = obj['@type'];
if (typeof type === 'string' && type.toLowerCase() === 'task') return true;
// CalDAV-created tasks may lack @type='Task' detect by task-specific fields
// CalDAV-created tasks may lack @type='Task' - detect by task-specific fields
if (type !== 'Event' && (
('progress' in obj && typeof obj.progress === 'string') ||
('due' in obj && obj.due != null) ||
@@ -189,7 +189,7 @@ const CALENDAR_TASK_PROPERTIES = [
'useDefaultAlerts',
'alerts',
'relatedTo',
'percentComplete', // Task-only per RFC 8984 §5.2.4 used in detection heuristic
'percentComplete', // Task-only per RFC 8984 §5.2.4 - used in detection heuristic
] as const;
/**
@@ -209,7 +209,7 @@ function cleanRecurrenceRules(event: Record<string, unknown>): void {
if (rules === undefined) continue;
delete event[pluralKey];
if (!Array.isArray(rules)) {
// null means "remove recurrence" pass through with the correct key
// null means "remove recurrence" - pass through with the correct key
event[singularKey] = rules;
continue;
}
@@ -398,7 +398,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers });
}
// Handle 429 rate limiting stop immediately, do not retry
// Handle 429 rate limiting - stop immediately, do not retry
if (response.status === 429) {
const retryAfterMs = JMAPClient.parseRetryAfter(response);
this.setRateLimited(retryAfterMs);
@@ -414,7 +414,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers: retryHeaders });
}
} else if (this.authMode === 'basic' && !this.reconnecting && url !== `${this.serverUrl}/.well-known/jmap`) {
// JMAP session may have expired re-establish and retry once
// JMAP session may have expired - re-establish and retry once
this.reconnecting = true;
try {
await this.refreshSession();
@@ -422,7 +422,7 @@ export class JMAPClient implements IJMAPClient {
const retryHeaders = { ...init?.headers as Record<string, string>, 'Authorization': this.authHeader };
response = await fetch(url, { ...init, headers: retryHeaders });
} catch {
// Session refresh failed if TOTP was used, try re-auth with fresh TOTP
// Session refresh failed - if TOTP was used, try re-auth with fresh TOTP
if (this.onTotpRequired && this.basePassword) {
try {
const newTotp = await this.onTotpRequired();
@@ -434,7 +434,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers: retryHeaders });
}
} catch {
// TOTP re-auth also failed return original 401
// TOTP re-auth also failed - return original 401
}
}
} finally {
@@ -697,7 +697,7 @@ export class JMAPClient implements IJMAPClient {
if (rawMailboxes.length >= maxObjects) {
debug.warn('jmap',
`[JMAP Mailbox] Response contains ${rawMailboxes.length} mailboxes which equals maxObjectsInGet (${maxObjects}). ` +
`Some mailboxes may be missing nested folders could appear orphaned at root level.`
`Some mailboxes may be missing - nested folders could appear orphaned at root level.`
);
}
@@ -850,7 +850,7 @@ export class JMAPClient implements IJMAPClient {
if (response.methodResponses?.[1]?.[0] === "Email/get" && getResponse) {
const emails = (getResponse.list || []) as Email[];
// Sort client-side as safety net some servers may not honour
// Sort client-side as safety net - some servers may not honour
// the query sort for large mailboxes without additional filters.
emails.sort((a: Email, b: Email) =>
new Date(b.receivedAt).getTime() - new Date(a.receivedAt).getTime()
@@ -2814,7 +2814,7 @@ export class JMAPClient implements IJMAPClient {
for (const [id, account] of Object.entries(this.accounts)) {
if (id === primaryId) continue;
// Include accounts that either advertise calendar capability
// or are non-personal (shared/group) accounts Stalwart doesn't
// or are non-personal (shared/group) accounts - Stalwart doesn't
// always advertise capabilities on group accounts even when they
// have calendar resources.
if (account.accountCapabilities?.["urn:ietf:params:jmap:calendars"] || !account.isPersonal) {
@@ -2830,7 +2830,7 @@ export class JMAPClient implements IJMAPClient {
for (const [id, account] of Object.entries(this.accounts)) {
if (id === primaryId) continue;
// Include accounts that either advertise contacts capability
// or are non-personal (shared/group) accounts Stalwart doesn't
// or are non-personal (shared/group) accounts - Stalwart doesn't
// always advertise capabilities on group accounts even when they
// have contact resources.
if (account.accountCapabilities?.["urn:ietf:params:jmap:contacts"] || !account.isPersonal) {
@@ -3976,7 +3976,7 @@ export class JMAPClient implements IJMAPClient {
// CalDAV-created tasks (e.g. Thunderbird) may lack @type or have @type
// set to something other than 'Event'. Detect them by the presence of
// task-specific keys (due, progress, percentComplete), which RFC 8984 §5.2
// defines as Task-only a VEVENT will never include them in the response.
// defines as Task-only - a VEVENT will never include them in the response.
// We check for key presence (even if null) because Stalwart may return null
// instead of the RFC defaults (e.g. progress default is "needs-action").
// @see https://www.rfc-editor.org/rfc/rfc8984#section-5.2
@@ -4060,7 +4060,7 @@ export class JMAPClient implements IJMAPClient {
if (!createdId) {
debug.warn('tasks', 'CalendarTask/create no id in server response');
debug.groupEnd();
throw new Error("Failed to create task no id returned");
throw new Error("Failed to create task - no id returned");
}
// Fetch back with task-specific properties
@@ -4230,7 +4230,7 @@ export class JMAPClient implements IJMAPClient {
async createFileDirectory(name: string, parentId: string | null): Promise<FileNode> {
const accountId = this.getFilesAccountId();
// Stalwart requires a blobId even for directories upload an empty blob
// Stalwart requires a blobId even for directories - upload an empty blob
const emptyBlob = new File([], name, { type: 'application/x-directory' });
const { blobId } = await this.uploadBlob(emptyBlob);
@@ -4489,7 +4489,7 @@ export class JMAPClient implements IJMAPClient {
this.stopSSEPingMonitor();
// Stream ended reconnect unless we were intentionally closed
// Stream ended - reconnect unless we were intentionally closed
if (this.sseAbortController && !this.sseAbortController.signal.aborted) {
this.scheduleSSEReconnect();
}
@@ -4512,7 +4512,7 @@ export class JMAPClient implements IJMAPClient {
const change = JSON.parse(dataLines.join('\n')) as StateChange;
this.stateChangeCallback?.(change);
} catch {
// Malformed SSE data ignore
// Malformed SSE data - ignore
}
}
}
@@ -4671,7 +4671,7 @@ export class JMAPClient implements IJMAPClient {
this.stopSSEPingMonitor();
this.ssePingTimer = setInterval(() => {
if (Date.now() - this.lastSSEActivity > JMAPClient.SSE_PING_TIMEOUT) {
// SSE connection is stale abort and reconnect
// SSE connection is stale - abort and reconnect
this.stopSSEPingMonitor();
if (this.sseAbortController) {
this.sseAbortController.abort();
@@ -4693,7 +4693,7 @@ export class JMAPClient implements IJMAPClient {
if (typeof document !== 'undefined') {
this.visibilityHandler = () => {
if (!document.hidden) {
// Tab became visible immediately check for state changes
// Tab became visible - immediately check for state changes
this.checkForStateChanges();
}
};
@@ -4702,7 +4702,7 @@ export class JMAPClient implements IJMAPClient {
if (typeof window !== 'undefined') {
this.onlineHandler = () => {
// Network reconnected reconnect SSE or force a poll
// Network reconnected - reconnect SSE or force a poll
const eventSourceUrl = this.getEventSourceUrl();
if (eventSourceUrl && !this.sseAbortController) {
this.connectSSE(eventSourceUrl);
+1 -1
View File
@@ -39,7 +39,7 @@ export interface Email {
// S/MIME support
blobId?: string;
bodyStructure?: EmailBodyPart;
// Unified mailbox support set when displaying emails from multiple accounts
// Unified mailbox support - set when displaying emails from multiple accounts
accountId?: string;
accountLabel?: string;
}
+5 -5
View File
@@ -112,7 +112,7 @@ function createPluginLogger(pluginId: string) {
export interface PluginAPI {
plugin: { id: string; version: string; settings: Record<string, unknown> };
/** Localisation API register translations and call t() to get strings */
/** Localisation API - register translations and call t() to get strings */
i18n: PluginI18n;
ui: {
registerToolbarAction: (action: ToolbarAction) => Disposable;
@@ -154,7 +154,7 @@ export interface PluginHooksAPI {
onEmailClose: (handler: () => void) => Disposable;
onEmailContentRender: (handler: (...args: unknown[]) => unknown) => Disposable;
onThreadExpand: (handler: (...args: unknown[]) => unknown) => Disposable;
/** Intercept receives ComposeOptions, may mutate fields, return false to cancel */
/** Intercept - receives ComposeOptions, may mutate fields, return false to cancel */
onBeforeCompose: (handler: (options: import('./plugin-types').ComposeOptions) => boolean | void | Promise<boolean | void>) => Disposable;
onComposerOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
onBeforeEmailSend: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -184,7 +184,7 @@ export interface PluginHooksAPI {
onNewEmailReceived: (handler: (...args: unknown[]) => unknown) => Disposable;
onPushConnectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
onQuotaChange: (handler: (...args: unknown[]) => unknown) => Disposable;
/** Intercept receives MailtoContext, return false to prevent the system mail client */
/** Intercept - receives MailtoContext, return false to prevent the system mail client */
onMailtoIntercept: (handler: (ctx: import('./plugin-types').MailtoContext) => boolean | void | Promise<boolean | void>) => Disposable;
// Calendar
onCalendarEventOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -228,7 +228,7 @@ export interface PluginHooksAPI {
onDirectoryCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
onBeforeFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
onAfterFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
/** Intercept receives { file: FileResourceView, newName: string }, return false to cancel */
/** Intercept - receives { file: FileResourceView, newName: string }, return false to cancel */
onBeforeFileRename: (handler: (ctx: { file: import('./plugin-types').FileResourceView; newName: string }) => boolean | void | Promise<boolean | void>) => Disposable;
onFileRename: (handler: (...args: unknown[]) => unknown) => Disposable;
onFileMove: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -331,7 +331,7 @@ export interface PluginHooksAPI {
onSidebarAppChange: (handler: (...args: unknown[]) => unknown) => Disposable;
// Avatar
onAvatarResolve: (handler: (...args: unknown[]) => unknown) => Disposable;
// Render transform hook for email list row badges
// Render - transform hook for email list row badges
// Handler: (badges: EmailListBadge[], ctx: { emailId: string; email: EmailReadView }) => EmailListBadge[]
onEmailListItemRender: (handler: (...args: unknown[]) => unknown) => Disposable;
}
+8 -8
View File
@@ -1,4 +1,4 @@
// Plugin Hook Bus event bus system for plugin lifecycle hooks
// Plugin Hook Bus - event bus system for plugin lifecycle hooks
import type { Disposable } from './plugin-types';
@@ -108,7 +108,7 @@ export class HookBus<T extends (...args: any[]) => any> {
return this.handlers.length;
}
/** Fire all handlers (observer pattern no return values used) */
/** Fire all handlers (observer pattern - no return values used) */
async emit(...args: Parameters<T>): Promise<void> {
for (const { pluginId, handler } of this.handlers) {
if (pluginErrorTracker.isDisabled(pluginId)) continue;
@@ -132,7 +132,7 @@ export class HookBus<T extends (...args: any[]) => any> {
}
}
/** Fire handlers as interceptors any returning false cancels the operation */
/** Fire handlers as interceptors - any returning false cancels the operation */
async intercept(...args: Parameters<T>): Promise<boolean> {
for (const { pluginId, handler } of this.handlers) {
if (pluginErrorTracker.isDisabled(pluginId)) continue;
@@ -146,7 +146,7 @@ export class HookBus<T extends (...args: any[]) => any> {
return true;
}
/** Fire handlers as transforms each receives the output of the previous */
/** Fire handlers as transforms - each receives the output of the previous */
async transform<V>(initial: V, ...rest: unknown[]): Promise<V> {
let value = initial;
for (const { pluginId, handler } of this.handlers) {
@@ -172,7 +172,7 @@ export const emailHooks = {
onEmailClose: new HookBus(),
onEmailContentRender: new HookBus(),
onThreadExpand: new HookBus(),
// Intercept hook fires before the composer opens.
// Intercept hook - fires before the composer opens.
// Handlers receive ComposeOptions and may mutate fields in place.
// Return false to cancel opening the composer.
onBeforeCompose: new HookBus(),
@@ -204,7 +204,7 @@ export const emailHooks = {
onNewEmailReceived: new HookBus(),
onPushConnectionChange: new HookBus(),
onQuotaChange: new HookBus(),
// Intercept hook fired when a mailto: link is clicked.
// Intercept hook - fired when a mailto: link is clicked.
// Return false to prevent the browser from opening the system mail client.
onMailtoIntercept: new HookBus(),
};
@@ -261,7 +261,7 @@ export const fileHooks = {
onDirectoryCreate: new HookBus(),
onBeforeFileDelete: new HookBus(),
onAfterFileDelete: new HookBus(),
// Intercept hook fires before a file is renamed.
// Intercept hook - fires before a file is renamed.
// Receives { file: FileResourceView, newName: string }.
// Return false to cancel the rename.
onBeforeFileRename: new HookBus(),
@@ -423,7 +423,7 @@ export const avatarHooks = {
// §7.22 Render Hooks
export const renderHooks = {
// Transform hook runs for each visible email list row.
// Transform hook - runs for each visible email list row.
// Initial value: EmailListBadge[] (always starts as [])
// Second argument: { emailId: string; email: EmailReadView }
// Handlers return a new (or extended) badges array.
+2 -2
View File
@@ -1,4 +1,4 @@
// Plugin i18n registry manages per-plugin translation tables
// Plugin i18n registry - manages per-plugin translation tables
//
// Each plugin gets its own namespace keyed by:
// pluginId → locale → { messageKey → translated string }
@@ -105,7 +105,7 @@ export function createPluginI18n(pluginId: string) {
t(key: string, params?: Record<string, string | number>): string {
const template = resolve(pluginId, key);
if (template !== undefined) return interpolate(template, params);
return key; // never throw just return the key
return key; // never throw - just return the key
},
/** The current app locale (e.g. "en", "de", "fr") */
+1 -1
View File
@@ -1,4 +1,4 @@
// Plugin Loader loads and activates plugins via blob URL dynamic import
// Plugin Loader - loads and activates plugins via blob URL dynamic import
import type { InstalledPlugin, Disposable } from './plugin-types';
import { pluginStorage } from './plugin-storage';
+2 -2
View File
@@ -195,7 +195,7 @@ export interface KeyboardShortcut {
}
// ─── Read-Only View Types ────────────────────────────────────
// Projected views exposed to plugins no direct store references
// Projected views exposed to plugins - no direct store references
export interface EmailReadView {
id: string;
@@ -415,7 +415,7 @@ export interface ComposeOptions {
* A small visual indicator injected into an email list row via onEmailListItemRender.
*/
export interface EmailListBadge {
/** Stable unique key within the plugin used as React key */
/** Stable unique key within the plugin - used as React key */
key: string;
/** Short label text displayed in the badge */
label: string;
+1 -1
View File
@@ -116,7 +116,7 @@ function checkJSSecurity(code: string): string[] {
const warnings: string[] = [];
for (const { pattern, label } of SUSPICIOUS_JS_PATTERNS) {
if (pattern.test(code)) {
warnings.push(`Contains ${label} review for security`);
warnings.push(`Contains ${label} - review for security`);
}
pattern.lastIndex = 0; // reset regex
}
+4 -4
View File
@@ -24,7 +24,7 @@ const INDEX_TO_DAY: string[] = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'];
* store can use it for mutations.
*
* Non-recurring events are returned as-is. For recurring events the master is
* **not** returned only expanded instances within the range.
* **not** returned - only expanded instances within the range.
*/
export function expandRecurringEvents(
events: CalendarEvent[],
@@ -46,7 +46,7 @@ export function expandRecurringEvents(
}
for (const event of events) {
// Skip override instances returned by the server they belong to a
// Skip override instances returned by the server - they belong to a
// master recurring event and are already handled via recurrenceOverrides.
if (event.recurrenceId && event.uid && recurringUids.has(event.uid)) {
continue;
@@ -165,7 +165,7 @@ function createOccurrence(
}
// ---------------------------------------------------------------------------
// §3.3.3.1 Implicit byX property addition
// §3.3.3.1 - Implicit byX property addition
// ---------------------------------------------------------------------------
function addImplicitByX(
rule: CalendarRecurrenceRule,
@@ -211,7 +211,7 @@ function addImplicitByX(
}
// ---------------------------------------------------------------------------
// §3.3.3.1 Generate occurrence dates
// §3.3.3.1 - Generate occurrence dates
// ---------------------------------------------------------------------------
function generateDates(
eventStart: Date,
+4 -4
View File
@@ -19,7 +19,7 @@ function makeBulwarkRule(overrides: Partial<FilterRule> = {}): FilterRule {
}
describe('external rule preservation (issue #201)', () => {
describe('parser external rule recognition', () => {
describe('parser - external rule recognition', () => {
it('parses a Roundcube-style rule with "# rule:[Name]" comment', () => {
const script = `require ["fileinto"];\n\n# rule:[Archive Newsletters]\nif header :contains "List-Id" "news" {\n fileinto "Newsletters";\n}\n`;
const result = parseScript(script);
@@ -84,7 +84,7 @@ describe('external rule preservation (issue #201)', () => {
});
});
describe('parser mixed Bulwark + external', () => {
describe('parser - mixed Bulwark + external', () => {
it('returns Bulwark rules from metadata and external rules from the rest', () => {
const bulwark = [makeBulwarkRule({ name: 'Bulwark A' })];
const bulwarkScript = generateScript(bulwark);
@@ -113,7 +113,7 @@ describe('external rule preservation (issue #201)', () => {
});
});
describe('generator external splice', () => {
describe('generator - external splice', () => {
it('appends external rawBlocks verbatim after Bulwark-managed output', () => {
const externalRule: FilterRule = {
id: 'ext-0',
@@ -253,7 +253,7 @@ describe('external rule preservation (issue #201)', () => {
const externalBefore = parsed.rules.filter(r => r.origin === 'external');
expect(externalBefore).toHaveLength(1);
// Simulate a user edit update the Bulwark rule name
// Simulate a user edit - update the Bulwark rule name
const edited = parsed.rules.map(r => (r.origin === 'external' || r.origin === 'opaque' ? r : { ...r, name: 'Mine (edited)' }));
const regenerated = generateScript(edited, parsed.vacation, { externalRequires: parsed.externalRequires });
const reparsed = parseScript(regenerated);
@@ -17,7 +17,7 @@ if anyof(header :is "From" "ceo@company.com", header :is "From" "board@company.c
# --- External rules (managed outside Bulwark) ---
# rule:[Finance auto-file invoices]
# rule:[Finance - auto-file invoices]
if allof(header :contains "From" "billing@", header :contains "Subject" "invoice") {
fileinto :copy "Finance/Invoices";
keep;
+4 -4
View File
@@ -233,7 +233,7 @@ function extractRequireTokens(stmt: string): string[] {
/**
* Extract the last contiguous block of comments immediately preceding a
* statement comments separated from the statement by a blank line are not
* statement - comments separated from the statement by a blank line are not
* considered its leading commentary (they likely belong to the previous
* block, e.g. a trailing "# Nextcloud Mail - end" marker).
*/
@@ -341,7 +341,7 @@ function parseAtom(raw: string): FilterCondition | null {
} else if (tag === 'is') {
comparator = negated ? 'not_is' : 'is';
} else {
// :matches distinguish starts_with / ends_with / matches
// :matches - distinguish starts_with / ends_with / matches
const starPositions = [...value].reduce<number[]>((acc, ch, idx) => (ch === '*' ? [...acc, idx] : acc), []);
if (starPositions.length === 1 && starPositions[0] === value.length - 1) {
comparator = 'starts_with';
@@ -591,7 +591,7 @@ export function parseScript(content: string): ParseResult {
};
}
// No metadata check vacation-only first
// No metadata - check vacation-only first
const vacationOnly = detectVacationOnlyScript(content);
if (vacationOnly) return vacationOnly;
@@ -599,7 +599,7 @@ export function parseScript(content: string): ParseResult {
const external = parseExternalRules(content, 'ext');
if (!external.hasContent) {
// Entirely empty or whitespace/comments only treat as empty, editable.
// Entirely empty or whitespace/comments only - treat as empty, editable.
return { rules: [], isOpaque: false, externalRequires: [] };
}
+1 -1
View File
@@ -187,7 +187,7 @@ function extractEmailAddresses(cert: pkijs.Certificate): string[] {
names = gn.names;
}
} catch {
// Malformed SAN skip gracefully
// Malformed SAN - skip gracefully
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ import * as pkijs from 'pkijs';
// webcrypto-liner exports a Crypto constructor at runtime that extends native
// Web Crypto with legacy algorithms (3DES, etc.). Its type declarations only
// expose the type alias, so we import the module dynamically and cast.
// Import the ES module build directly the package's "browser" field points
// Import the ES module build directly - the package's "browser" field points
// to a shim-only build that has no named exports (no setCrypto, Crypto, etc.).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const liner = require('webcrypto-liner/build/index.es.js') as {
@@ -55,7 +55,7 @@ function pbeConfig(oid: string): { keyLen: number; ivLen: number; algName: strin
}
/**
* PKCS#12 key derivation RFC 7292, Appendix B.
* PKCS#12 key derivation - RFC 7292, Appendix B.
*
* @param password BMP-encoded password (with trailing 0x00 0x00)
* @param salt raw salt bytes
+1 -1
View File
@@ -275,7 +275,7 @@ function cmsToBase64Blob(data: Blob | ArrayBuffer | Uint8Array): Blob {
} else if (data instanceof ArrayBuffer) {
bytes = new Uint8Array(data);
} else {
// Blob we need sync; caller should have converted. Fallback to empty.
// Blob - we need sync; caller should have converted. Fallback to empty.
bytes = new Uint8Array(0);
}
const b64 = base64Encode(bytes.buffer as ArrayBuffer);
+1 -1
View File
@@ -106,7 +106,7 @@ export async function exportPkcs12(
parsedValue: {
safeContents: [
{
privacyMode: 0, // no extra encryption key bag is already shrouded
privacyMode: 0, // no extra encryption - key bag is already shrouded
value: new pkijs.SafeContents({
safeBags: [keyBagSafe],
}),
+1 -1
View File
@@ -89,7 +89,7 @@ export async function importPkcs12(
cert = certBag.parsedValue;
der = cert.toSchema(true).toBER(false);
} else if (certBag.certId === '1.2.840.113549.1.9.22.1' && certBag.certValue) {
// x509Certificate extract DER from the OCTET STRING
// x509Certificate - extract DER from the OCTET STRING
const certDerBytes = (certBag.certValue as asn1js.OctetString).valueBlock.valueHexView;
const certAsn1 = asn1js.fromBER(certDerBytes);
if (certAsn1.offset !== -1) {
+5 -5
View File
@@ -78,7 +78,7 @@ export async function smimeDecrypt(input: DecryptionInput): Promise<DecryptionRe
continue;
}
}
continue; // Key exists but isn't unlocked skip, caller should unlock first
continue; // Key exists but isn't unlocked - skip, caller should unlock first
}
try {
@@ -170,7 +170,7 @@ export function normalizeCmsBytes(raw: ArrayBuffer): ArrayBuffer {
const bytes = new Uint8Array(raw);
// Already valid DER starts with ASN.1 SEQUENCE tag
// Already valid DER - starts with ASN.1 SEQUENCE tag
if (bytes[0] === 0x30) {
return raw;
}
@@ -272,19 +272,19 @@ export function normalizeCmsBytes(raw: ArrayBuffer): ArrayBuffer {
}
}
// Not decodable return original bytes
// Not decodable - return original bytes
return raw;
}
function parseContentInfo(der: ArrayBuffer): pkijs.ContentInfo {
const asn1 = asn1js.fromBER(der);
if (asn1.offset === -1) {
throw new Error('Invalid ASN.1 data cannot parse CMS envelope');
throw new Error('Invalid ASN.1 data - cannot parse CMS envelope');
}
try {
return new pkijs.ContentInfo({ schema: asn1.result });
} catch {
throw new Error('Invalid ASN.1 data cannot parse CMS envelope');
throw new Error('Invalid ASN.1 data - cannot parse CMS envelope');
}
}
+1 -1
View File
@@ -69,7 +69,7 @@ export function detectSmime(
supported: true,
};
}
// Generic pkcs7-mime without explicit smime-type try bodyStructure
// Generic pkcs7-mime without explicit smime-type - try bodyStructure
const part = findCmsPart(bodyStructure, null);
if (part) {
const partType = inferSmimeType(part);
+1 -1
View File
@@ -6,7 +6,7 @@ import { parseCertificateDer } from './certificate-utils';
* Produce an opaque CMS SignedData wrapping the given MIME content.
*
* Content type: application/pkcs7-mime; smime-type=signed-data
* This is the "opaque" form the content is embedded inside the CMS structure.
* This is the "opaque" form - the content is embedded inside the CMS structure.
*/
export async function smimeSign(
mimeBytes: Uint8Array,
+1 -1
View File
@@ -132,7 +132,7 @@ export async function smimeVerify(
function parseContentInfo(der: ArrayBuffer): pkijs.ContentInfo {
const asn1 = asn1js.fromBER(der);
if (asn1.offset === -1) {
throw new Error('Invalid ASN.1 data cannot parse CMS structure');
throw new Error('Invalid ASN.1 data - cannot parse CMS structure');
}
return new pkijs.ContentInfo({ schema: asn1.result });
}
+1 -1
View File
@@ -52,7 +52,7 @@ export function validateThemeSelectors(css: string): string[] {
// Inside @media blocks, also allow :root and .dark
if (selector === ':root' || selector === '.dark') continue;
warnings.push(`Non-standard selector "${selector}" themes should only use :root and .dark`);
warnings.push(`Non-standard selector "${selector}" - themes should only use :root and .dark`);
}
return warnings;
+4 -4
View File
@@ -125,7 +125,7 @@ function readMAPIFixedValue(r: BinaryReader, propType: number): Uint8Array | num
case PT_CLSID:
return r.readBytes(16);
default:
// Unknown/unsupported type try to read as fixed 4 bytes
// Unknown/unsupported type - try to read as fixed 4 bytes
if (r.remaining >= 4) {
return r.readBytes(4);
}
@@ -268,7 +268,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
attrCount++;
if (attrLen > r.remaining - 2) {
debug.warn('email', 'Attribute #' + attrCount + ': truncated data need', attrLen, 'bytes but only', r.remaining - 2, 'available');
debug.warn('email', 'Attribute #' + attrCount + ': truncated data - need', attrLen, 'bytes but only', r.remaining - 2, 'available');
break;
}
@@ -316,7 +316,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
}
} else if (level === LVL_ATTACHMENT) {
if (attrID === attAttachRenddata) {
// Start of a new attachment flush previous
// Start of a new attachment - flush previous
if (curAttach?.data) {
debug.log('email', ' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
result.attachments.push({
@@ -375,7 +375,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
});
}
debug.log('email', 'TNEF parsing complete body:', !!result.body, ', htmlBody:', !!result.htmlBody, ', attachments:', result.attachments.length);
debug.log('email', 'TNEF parsing complete - body:', !!result.body, ', htmlBody:', !!result.htmlBody, ', attachments:', result.attachments.length);
if (result.attachments.length > 0) {
debug.table(result.attachments.map(a => ({ name: a.name, mimeType: a.mimeType, size: a.data.byteLength })), 'email');
}
+1 -1
View File
@@ -139,7 +139,7 @@ function deduplicateMailboxes(mailboxes: Mailbox[]): Mailbox[] {
return;
}
// Never deduplicate nested mailboxes only root-level folders can be
// Never deduplicate nested mailboxes - only root-level folders can be
// duplicates of role-based mailboxes. Removing a nested folder that happens
// to share a name with a role folder (e.g. a subfolder named "Sent") would
// orphan its children to root level. (GitHub #118)