fix: standardize punctuation
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }),
|
||||
];
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ function extractEmailAddresses(cert: pkijs.Certificate): string[] {
|
||||
names = gn.names;
|
||||
}
|
||||
} catch {
|
||||
// Malformed SAN — skip gracefully
|
||||
// Malformed SAN - skip gracefully
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user