feat: logging to include categories for better log management
This commit is contained in:
@@ -765,7 +765,7 @@ export default function CalendarPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('Calendar visibility summary', {
|
debug.log('calendar', 'Calendar visibility summary', {
|
||||||
totalEvents: events.length,
|
totalEvents: events.length,
|
||||||
visibleEvents: visibleEvents.length,
|
visibleEvents: visibleEvents.length,
|
||||||
hiddenEvents: hiddenEvents.length,
|
hiddenEvents: hiddenEvents.length,
|
||||||
|
|||||||
+11
-11
@@ -352,13 +352,13 @@ export default function Home() {
|
|||||||
|
|
||||||
if (pushEnabled) {
|
if (pushEnabled) {
|
||||||
setPushConnected(true);
|
setPushConnected(true);
|
||||||
debug.log('[Push] Push notifications successfully enabled');
|
debug.log('push', '[Push] Push notifications successfully enabled');
|
||||||
} else {
|
} else {
|
||||||
debug.log('[Push] Push notifications not available on this server');
|
debug.log('push', '[Push] Push notifications not available on this server');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Push notifications are optional - don't break the app if they fail
|
// Push notifications are optional - don't break the app if they fail
|
||||||
debug.log('[Push] Failed to setup push notifications:', error);
|
debug.log('push', '[Push] Failed to setup push notifications:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading email data:', error);
|
console.error('Error loading email data:', error);
|
||||||
@@ -392,7 +392,7 @@ export default function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clear any existing timeout when email changes
|
// Clear any existing timeout when email changes
|
||||||
if (markAsReadTimeoutRef.current) {
|
if (markAsReadTimeoutRef.current) {
|
||||||
debug.log('[Mark as Read] Clearing previous timeout');
|
debug.log('email', '[Mark as Read] Clearing previous timeout');
|
||||||
clearTimeout(markAsReadTimeoutRef.current);
|
clearTimeout(markAsReadTimeoutRef.current);
|
||||||
markAsReadTimeoutRef.current = null;
|
markAsReadTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -404,20 +404,20 @@ export default function Home() {
|
|||||||
|
|
||||||
// Get current setting value
|
// Get current setting value
|
||||||
const markAsReadDelay = useSettingsStore.getState().markAsReadDelay;
|
const markAsReadDelay = useSettingsStore.getState().markAsReadDelay;
|
||||||
debug.log('[Mark as Read] Delay setting:', markAsReadDelay, 'ms for email:', selectedEmail.id);
|
debug.log('email', '[Mark as Read] Delay setting:', markAsReadDelay, 'ms for email:', selectedEmail.id);
|
||||||
|
|
||||||
if (markAsReadDelay === -1) {
|
if (markAsReadDelay === -1) {
|
||||||
// Never mark as read automatically
|
// Never mark as read automatically
|
||||||
debug.log('[Mark as Read] Never mode - email will stay unread');
|
debug.log('email', '[Mark as Read] Never mode - email will stay unread');
|
||||||
} else if (markAsReadDelay === 0) {
|
} else if (markAsReadDelay === 0) {
|
||||||
// Mark as read instantly
|
// Mark as read instantly
|
||||||
debug.log('[Mark as Read] Instant mode - marking as read now');
|
debug.log('email', '[Mark as Read] Instant mode - marking as read now');
|
||||||
markAsRead(client, selectedEmail.id, true);
|
markAsRead(client, selectedEmail.id, true);
|
||||||
} else {
|
} else {
|
||||||
// Mark as read after delay
|
// Mark as read after delay
|
||||||
debug.log('[Mark as Read] Delayed mode - will mark as read in', markAsReadDelay, 'ms');
|
debug.log('email', '[Mark as Read] Delayed mode - will mark as read in', markAsReadDelay, 'ms');
|
||||||
markAsReadTimeoutRef.current = setTimeout(() => {
|
markAsReadTimeoutRef.current = setTimeout(() => {
|
||||||
debug.log('[Mark as Read] Timeout fired - marking as read now');
|
debug.log('email', '[Mark as Read] Timeout fired - marking as read now');
|
||||||
markAsRead(client, selectedEmail.id, true);
|
markAsRead(client, selectedEmail.id, true);
|
||||||
markAsReadTimeoutRef.current = null;
|
markAsReadTimeoutRef.current = null;
|
||||||
}, markAsReadDelay);
|
}, markAsReadDelay);
|
||||||
@@ -426,7 +426,7 @@ export default function Home() {
|
|||||||
// Cleanup on unmount or when dependencies change
|
// Cleanup on unmount or when dependencies change
|
||||||
return () => {
|
return () => {
|
||||||
if (markAsReadTimeoutRef.current) {
|
if (markAsReadTimeoutRef.current) {
|
||||||
debug.log('[Mark as Read] Cleanup - clearing timeout');
|
debug.log('email', '[Mark as Read] Cleanup - clearing timeout');
|
||||||
clearTimeout(markAsReadTimeoutRef.current);
|
clearTimeout(markAsReadTimeoutRef.current);
|
||||||
markAsReadTimeoutRef.current = null;
|
markAsReadTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -441,7 +441,7 @@ export default function Home() {
|
|||||||
if (emailNotificationsEnabled && emailNotificationSound) {
|
if (emailNotificationsEnabled && emailNotificationSound) {
|
||||||
playNotificationSound(notificationSoundChoice);
|
playNotificationSound(notificationSoundChoice);
|
||||||
}
|
}
|
||||||
debug.log('New email received:', newEmailNotification.subject);
|
debug.log('email', 'New email received:', newEmailNotification.subject);
|
||||||
clearNewEmailNotification();
|
clearNewEmailNotification();
|
||||||
}
|
}
|
||||||
}, [newEmailNotification, clearNewEmailNotification]);
|
}, [newEmailNotification, clearNewEmailNotification]);
|
||||||
|
|||||||
@@ -1820,12 +1820,12 @@ export function EmailViewer({
|
|||||||
|
|
||||||
const tnefAtt = email.attachments.find(att => isTnefAttachment(att.name, att.type));
|
const tnefAtt = email.attachments.find(att => isTnefAttachment(att.name, att.type));
|
||||||
if (!tnefAtt?.blobId) {
|
if (!tnefAtt?.blobId) {
|
||||||
debug.log('TNEF: No winmail.dat attachment found in email', email?.id);
|
debug.log('email', 'TNEF: No winmail.dat attachment found in email', email?.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.group('TNEF Processing');
|
debug.group('TNEF Processing', 'email');
|
||||||
debug.log('Found TNEF attachment:', tnefAtt.name, 'type:', tnefAtt.type, 'blobId:', tnefAtt.blobId, 'size:', tnefAtt.size);
|
debug.log('email', 'Found TNEF attachment:', tnefAtt.name, 'type:', tnefAtt.type, 'blobId:', tnefAtt.blobId, 'size:', tnefAtt.size);
|
||||||
|
|
||||||
// Check if the email already has a usable HTML body with real content
|
// Check if the email already has a usable HTML body with real content
|
||||||
// Outlook often forwards TNEF emails with an HTML body that's just Word
|
// Outlook often forwards TNEF emails with an HTML body that's just Word
|
||||||
@@ -1835,46 +1835,46 @@ export function EmailViewer({
|
|||||||
let hasRealHtmlBody = !!htmlValue;
|
let hasRealHtmlBody = !!htmlValue;
|
||||||
if (hasRealHtmlBody && htmlValue && isHtmlBodyEffectivelyEmpty(htmlValue)) {
|
if (hasRealHtmlBody && htmlValue && isHtmlBodyEffectivelyEmpty(htmlValue)) {
|
||||||
hasRealHtmlBody = false;
|
hasRealHtmlBody = false;
|
||||||
debug.log('TNEF: Email HTML body is effectively empty (only boilerplate/whitespace), treating as no body');
|
debug.log('email', 'TNEF: Email HTML body is effectively empty (only boilerplate/whitespace), treating as no body');
|
||||||
}
|
}
|
||||||
if (hasRealHtmlBody) {
|
if (hasRealHtmlBody) {
|
||||||
debug.log('TNEF: Email has real HTML body, will extract attachments only');
|
debug.log('email', 'TNEF: Email has real HTML body, will extract attachments only');
|
||||||
} else {
|
} else {
|
||||||
debug.log('TNEF: Email has no usable HTML body, proceeding with full TNEF extraction');
|
debug.log('email', 'TNEF: Email has no usable HTML body, proceeding with full TNEF extraction');
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function processTnef() {
|
async function processTnef() {
|
||||||
try {
|
try {
|
||||||
debug.time('TNEF fetch blob');
|
debug.time('TNEF fetch blob', 'email');
|
||||||
const blobBytes = await client!.fetchBlobArrayBuffer(tnefAtt!.blobId!);
|
const blobBytes = await client!.fetchBlobArrayBuffer(tnefAtt!.blobId!);
|
||||||
debug.timeEnd('TNEF fetch blob');
|
debug.timeEnd('TNEF fetch blob', 'email');
|
||||||
debug.log('TNEF: Fetched blob, size:', blobBytes.byteLength, 'bytes');
|
debug.log('email', 'TNEF: Fetched blob, size:', blobBytes.byteLength, 'bytes');
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
debug.log('TNEF: Processing cancelled after fetch');
|
debug.log('email', 'TNEF: Processing cancelled after fetch');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (blobBytes.byteLength === 0) {
|
if (blobBytes.byteLength === 0) {
|
||||||
debug.warn('TNEF: Fetched blob is empty (0 bytes)');
|
debug.warn('email', 'TNEF: Fetched blob is empty (0 bytes)');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tnefData = new Uint8Array(blobBytes);
|
const tnefData = new Uint8Array(blobBytes);
|
||||||
debug.time('TNEF parse');
|
debug.time('TNEF parse', 'email');
|
||||||
const parsed = parseTnef(tnefData);
|
const parsed = parseTnef(tnefData);
|
||||||
debug.timeEnd('TNEF parse');
|
debug.timeEnd('TNEF parse', 'email');
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
debug.log('TNEF: Processing cancelled after parse');
|
debug.log('email', 'TNEF: Processing cancelled after parse');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('TNEF parse result — htmlBody:', !!parsed.htmlBody, '(' + (parsed.htmlBody?.length ?? 0) + ' chars)', ', body:', !!parsed.body, '(' + (parsed.body?.length ?? 0) + ' chars)', ', attachments:', parsed.attachments.length);
|
debug.log('email', 'TNEF parse result — htmlBody:', !!parsed.htmlBody, '(' + (parsed.htmlBody?.length ?? 0) + ' chars)', ', body:', !!parsed.body, '(' + (parsed.body?.length ?? 0) + ' chars)', ', attachments:', parsed.attachments.length);
|
||||||
|
|
||||||
if (parsed.htmlBody && !hasRealHtmlBody) {
|
if (parsed.htmlBody && !hasRealHtmlBody) {
|
||||||
setTnefHtml(parsed.htmlBody);
|
setTnefHtml(parsed.htmlBody);
|
||||||
@@ -1884,11 +1884,11 @@ export function EmailViewer({
|
|||||||
}
|
}
|
||||||
if (parsed.attachments.length > 0) {
|
if (parsed.attachments.length > 0) {
|
||||||
setTnefAttachments(parsed.attachments);
|
setTnefAttachments(parsed.attachments);
|
||||||
debug.log('TNEF extracted attachments:', parsed.attachments.map(a => a.name + ' (' + a.mimeType + ', ' + a.data.byteLength + ' bytes)').join(', '));
|
debug.log('email', 'TNEF extracted attachments:', parsed.attachments.map(a => a.name + ' (' + a.mimeType + ', ' + a.data.byteLength + ' bytes)').join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed.htmlBody && !parsed.body && parsed.attachments.length === 0) {
|
if (!parsed.htmlBody && !parsed.body && parsed.attachments.length === 0) {
|
||||||
debug.warn('TNEF: Parsing succeeded but no content was extracted — the winmail.dat may use an unsupported format');
|
debug.warn('email', 'TNEF: Parsing succeeded but no content was extracted — the winmail.dat may use an unsupported format');
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
@@ -1926,13 +1926,13 @@ export function EmailViewer({
|
|||||||
const hasRealText = !!textValue;
|
const hasRealText = !!textValue;
|
||||||
|
|
||||||
if (hasRealHtml || hasRealText) {
|
if (hasRealHtml || hasRealText) {
|
||||||
debug.log('Embedded RFC822: Outer email has real body content, not unwrapping');
|
debug.log('email', 'Embedded RFC822: Outer email has real body content, not unwrapping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.group('Embedded RFC822 Unwrapping');
|
debug.group('Embedded RFC822 Unwrapping', 'email');
|
||||||
debug.log('Found message/rfc822 attachment:', rfc822Att.name, 'blobId:', rfc822Att.blobId, 'size:', rfc822Att.size);
|
debug.log('email', 'Found message/rfc822 attachment:', rfc822Att.name, 'blobId:', rfc822Att.blobId, 'size:', rfc822Att.size);
|
||||||
debug.log('Outer email body is empty, will unwrap embedded email');
|
debug.log('email', 'Outer email body is empty, will unwrap embedded email');
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
@@ -1941,7 +1941,7 @@ export function EmailViewer({
|
|||||||
const blobBytes = await client!.fetchBlobArrayBuffer(rfc822Att!.blobId!);
|
const blobBytes = await client!.fetchBlobArrayBuffer(rfc822Att!.blobId!);
|
||||||
if (cancelled) { debug.groupEnd(); return; }
|
if (cancelled) { debug.groupEnd(); return; }
|
||||||
if (blobBytes.byteLength === 0) {
|
if (blobBytes.byteLength === 0) {
|
||||||
debug.warn('Embedded RFC822: Fetched blob is empty');
|
debug.warn('email', 'Embedded RFC822: Fetched blob is empty');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1951,7 +1951,7 @@ export function EmailViewer({
|
|||||||
const parsed = await parser.parse(new Uint8Array(blobBytes));
|
const parsed = await parser.parse(new Uint8Array(blobBytes));
|
||||||
if (cancelled) { debug.groupEnd(); return; }
|
if (cancelled) { debug.groupEnd(); return; }
|
||||||
|
|
||||||
debug.log('Embedded RFC822 parsed — html:', !!parsed.html, '(' + (parsed.html?.length ?? 0) + ' chars)',
|
debug.log('email', 'Embedded RFC822 parsed — html:', !!parsed.html, '(' + (parsed.html?.length ?? 0) + ' chars)',
|
||||||
', text:', !!parsed.text, '(' + (parsed.text?.length ?? 0) + ' chars)',
|
', text:', !!parsed.text, '(' + (parsed.text?.length ?? 0) + ' chars)',
|
||||||
', attachments:', parsed.attachments?.length ?? 0);
|
', attachments:', parsed.attachments?.length ?? 0);
|
||||||
|
|
||||||
@@ -1963,7 +1963,7 @@ export function EmailViewer({
|
|||||||
}
|
}
|
||||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
if (parsed.attachments && parsed.attachments.length > 0) {
|
||||||
setEmbeddedEmailAttachments(parsed.attachments as PostalMimeAttachment[]);
|
setEmbeddedEmailAttachments(parsed.attachments as PostalMimeAttachment[]);
|
||||||
debug.log('Embedded RFC822 attachments:', parsed.attachments.map(
|
debug.log('email', 'Embedded RFC822 attachments:', parsed.attachments.map(
|
||||||
a => (a.filename || 'unnamed') + ' (' + a.mimeType + ')'
|
a => (a.filename || 'unnamed') + ' (' + a.mimeType + ')'
|
||||||
).join(', '));
|
).join(', '));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { useConfig } from '@/hooks/use-config';
|
|||||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { usePolicyStore } from '@/stores/policy-store';
|
import { usePolicyStore } from '@/stores/policy-store';
|
||||||
|
import { ALL_DEBUG_CATEGORIES } from '@/stores/settings-store';
|
||||||
|
|
||||||
export function AdvancedSettings() {
|
export function AdvancedSettings() {
|
||||||
const t = useTranslations('settings.advanced');
|
const t = useTranslations('settings.advanced');
|
||||||
const tCommon = useTranslations('common');
|
const tCommon = useTranslations('common');
|
||||||
const { debugMode, senderFavicons, settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
const { debugMode, debugCategories, senderFavicons, settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
||||||
useSettingsStore();
|
useSettingsStore();
|
||||||
const { settingsSyncEnabled } = useConfig();
|
const { settingsSyncEnabled } = useConfig();
|
||||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||||
@@ -72,6 +73,30 @@ export function AdvancedSettings() {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Debug Categories */}
|
||||||
|
{debugMode && !isSettingHidden('debugMode') && isFeatureEnabled('debugModeEnabled') && (
|
||||||
|
<div className="ml-4 border-l-2 border-muted pl-4 space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">{t('debug_categories.description')}</p>
|
||||||
|
{ALL_DEBUG_CATEGORIES.map((cat) => (
|
||||||
|
<SettingItem
|
||||||
|
key={cat.id}
|
||||||
|
label={t(`debug_categories.${cat.labelKey}`)}
|
||||||
|
description={t(`debug_categories.${cat.labelKey}_description`)}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={debugCategories?.[cat.id] !== false}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateSetting('debugCategories', {
|
||||||
|
...debugCategories,
|
||||||
|
[cat.id]: checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Settings Sync */}
|
{/* Settings Sync */}
|
||||||
{settingsSyncEnabled && (
|
{settingsSyncEnabled && (
|
||||||
<SettingItem label={t('settings_sync.label')} description={t('settings_sync.description')}>
|
<SettingItem label={t('settings_sync.label')} description={t('settings_sync.description')}>
|
||||||
|
|||||||
+57
-24
@@ -1,25 +1,53 @@
|
|||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
|
import type { DebugCategory } from '@/stores/settings-store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug logger that respects the debugMode setting.
|
* Check if debug logging is enabled, optionally for a specific category.
|
||||||
|
* When a category is provided, both debugMode AND that category must be enabled.
|
||||||
|
*/
|
||||||
|
function isEnabled(category?: DebugCategory): boolean {
|
||||||
|
const state = useSettingsStore.getState();
|
||||||
|
if (!state.debugMode) return false;
|
||||||
|
if (!category) return true;
|
||||||
|
return state.debugCategories?.[category] !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug logger that respects the debugMode setting and category filters.
|
||||||
* Use this instead of console.log for conditional debug output.
|
* Use this instead of console.log for conditional debug output.
|
||||||
|
*
|
||||||
|
* Each method accepts an optional category as the first argument.
|
||||||
|
* When a category is provided, the message only logs if that category is enabled
|
||||||
|
* in Settings > Advanced > Debug Categories.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* debug.log('calendar', 'Event created', event); // Only logs when 'calendar' category is on
|
||||||
|
* debug.log('Uncategorized message'); // Logs whenever debugMode is on
|
||||||
*/
|
*/
|
||||||
export const debug = {
|
export const debug = {
|
||||||
/**
|
/**
|
||||||
* Log a debug message (only when debugMode is enabled)
|
* Log a debug message (only when debugMode is enabled and category is active)
|
||||||
*/
|
*/
|
||||||
log: (...args: unknown[]) => {
|
log: (categoryOrMsg: DebugCategory | unknown, ...args: unknown[]) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (typeof categoryOrMsg === 'string' && isCategoryKey(categoryOrMsg)) {
|
||||||
console.log('[DEBUG]', ...args);
|
if (isEnabled(categoryOrMsg)) {
|
||||||
|
console.log(`[DEBUG:${categoryOrMsg}]`, ...args);
|
||||||
|
}
|
||||||
|
} else if (isEnabled()) {
|
||||||
|
console.log('[DEBUG]', categoryOrMsg, ...args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a warning message (only when debugMode is enabled)
|
* Log a warning message (only when debugMode is enabled and category is active)
|
||||||
*/
|
*/
|
||||||
warn: (...args: unknown[]) => {
|
warn: (categoryOrMsg: DebugCategory | unknown, ...args: unknown[]) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (typeof categoryOrMsg === 'string' && isCategoryKey(categoryOrMsg)) {
|
||||||
console.warn('[DEBUG]', ...args);
|
if (isEnabled(categoryOrMsg)) {
|
||||||
|
console.warn(`[DEBUG:${categoryOrMsg}]`, ...args);
|
||||||
|
}
|
||||||
|
} else if (isEnabled()) {
|
||||||
|
console.warn('[DEBUG]', categoryOrMsg, ...args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -31,11 +59,11 @@ export const debug = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a collapsed console group (only when debugMode is enabled)
|
* Start a collapsed console group (only when debugMode is enabled and category is active)
|
||||||
*/
|
*/
|
||||||
group: (label: string) => {
|
group: (label: string, category?: DebugCategory) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (isEnabled(category)) {
|
||||||
console.group(`[DEBUG] ${label}`);
|
console.group(`[DEBUG${category ? ':' + category : ''}] ${label}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -43,35 +71,40 @@ export const debug = {
|
|||||||
* End a console group (only when debugMode is enabled)
|
* End a console group (only when debugMode is enabled)
|
||||||
*/
|
*/
|
||||||
groupEnd: () => {
|
groupEnd: () => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (isEnabled()) {
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a performance timer (only when debugMode is enabled)
|
* Start a performance timer (only when debugMode is enabled and category is active)
|
||||||
*/
|
*/
|
||||||
time: (label: string) => {
|
time: (label: string, category?: DebugCategory) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (isEnabled(category)) {
|
||||||
console.time(`[DEBUG] ${label}`);
|
console.time(`[DEBUG${category ? ':' + category : ''}] ${label}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End a performance timer (only when debugMode is enabled)
|
* End a performance timer (only when debugMode is enabled)
|
||||||
*/
|
*/
|
||||||
timeEnd: (label: string) => {
|
timeEnd: (label: string, category?: DebugCategory) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (isEnabled(category)) {
|
||||||
console.timeEnd(`[DEBUG] ${label}`);
|
console.timeEnd(`[DEBUG${category ? ':' + category : ''}] ${label}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a table (only when debugMode is enabled)
|
* Log a table (only when debugMode is enabled and category is active)
|
||||||
*/
|
*/
|
||||||
table: (data: unknown) => {
|
table: (data: unknown, category?: DebugCategory) => {
|
||||||
if (useSettingsStore.getState().debugMode) {
|
if (isEnabled(category)) {
|
||||||
console.table(data);
|
console.table(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CATEGORY_KEYS = new Set<string>(['jmap', 'calendar', 'tasks', 'auth', 'filters', 'email', 'push']);
|
||||||
|
function isCategoryKey(value: string): value is DebugCategory {
|
||||||
|
return CATEGORY_KEYS.has(value);
|
||||||
|
}
|
||||||
|
|||||||
+62
-62
@@ -670,12 +670,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (response.methodResponses?.[0]?.[0] === "Mailbox/get") {
|
if (response.methodResponses?.[0]?.[0] === "Mailbox/get") {
|
||||||
const rawMailboxes = (response.methodResponses[0][1].list || []) as JMAPMailbox[];
|
const rawMailboxes = (response.methodResponses[0][1].list || []) as JMAPMailbox[];
|
||||||
|
|
||||||
debug.log(`[JMAP Mailbox] getMailboxes returned ${rawMailboxes.length} mailboxes for account ${this.accountId}`);
|
debug.log('jmap', `[JMAP Mailbox] getMailboxes returned ${rawMailboxes.length} mailboxes for account ${this.accountId}`);
|
||||||
|
|
||||||
// Warn if response might be truncated
|
// Warn if response might be truncated
|
||||||
const maxObjects = this.getMaxObjectsInGet();
|
const maxObjects = this.getMaxObjectsInGet();
|
||||||
if (rawMailboxes.length >= maxObjects) {
|
if (rawMailboxes.length >= maxObjects) {
|
||||||
debug.warn(
|
debug.warn('jmap',
|
||||||
`[JMAP Mailbox] Response contains ${rawMailboxes.length} mailboxes which equals maxObjectsInGet (${maxObjects}). ` +
|
`[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.`
|
||||||
);
|
);
|
||||||
@@ -685,7 +685,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const returnedIds = new Set(rawMailboxes.map(mb => mb.id));
|
const returnedIds = new Set(rawMailboxes.map(mb => mb.id));
|
||||||
const missingParents = rawMailboxes.filter(mb => mb.parentId && !returnedIds.has(mb.parentId));
|
const missingParents = rawMailboxes.filter(mb => mb.parentId && !returnedIds.has(mb.parentId));
|
||||||
if (missingParents.length > 0) {
|
if (missingParents.length > 0) {
|
||||||
debug.warn(
|
debug.warn('jmap',
|
||||||
`[JMAP Mailbox] ${missingParents.length} mailbox(es) reference parentId not in response (will be orphaned):`,
|
`[JMAP Mailbox] ${missingParents.length} mailbox(es) reference parentId not in response (will be orphaned):`,
|
||||||
missingParents.map(mb => ({ id: mb.id, name: mb.name, parentId: mb.parentId }))
|
missingParents.map(mb => ({ id: mb.id, name: mb.name, parentId: mb.parentId }))
|
||||||
);
|
);
|
||||||
@@ -755,12 +755,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (response.methodResponses?.[0]?.[0] === "Mailbox/get") {
|
if (response.methodResponses?.[0]?.[0] === "Mailbox/get") {
|
||||||
const rawMailboxes = (response.methodResponses[0][1].list || []) as JMAPMailbox[];
|
const rawMailboxes = (response.methodResponses[0][1].list || []) as JMAPMailbox[];
|
||||||
|
|
||||||
debug.log(`[JMAP Mailbox] getAllMailboxes: account ${accountId} returned ${rawMailboxes.length} mailboxes (isPrimary: ${isPrimary})`);
|
debug.log('jmap', `[JMAP Mailbox] getAllMailboxes: account ${accountId} returned ${rawMailboxes.length} mailboxes (isPrimary: ${isPrimary})`);
|
||||||
|
|
||||||
// Warn if response might be truncated
|
// Warn if response might be truncated
|
||||||
const maxObjects = this.getMaxObjectsInGet();
|
const maxObjects = this.getMaxObjectsInGet();
|
||||||
if (rawMailboxes.length >= maxObjects) {
|
if (rawMailboxes.length >= maxObjects) {
|
||||||
debug.warn(
|
debug.warn('jmap',
|
||||||
`[JMAP Mailbox] Account ${accountId}: response contains ${rawMailboxes.length} mailboxes which equals maxObjectsInGet (${maxObjects}). ` +
|
`[JMAP Mailbox] Account ${accountId}: response contains ${rawMailboxes.length} mailboxes which equals maxObjectsInGet (${maxObjects}). ` +
|
||||||
`Some mailboxes may be missing.`
|
`Some mailboxes may be missing.`
|
||||||
);
|
);
|
||||||
@@ -1995,7 +1995,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
lines.push('END:VCALENDAR');
|
lines.push('END:VCALENDAR');
|
||||||
const icsContent = lines.join('\r\n') + '\r\n';
|
const icsContent = lines.join('\r\n') + '\r\n';
|
||||||
|
|
||||||
debug.log('[iMIP] Generated ICS:\n' + icsContent);
|
debug.log('calendar', '[iMIP] Generated ICS:\n' + icsContent);
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
ACCEPTED: 'Accepted',
|
ACCEPTED: 'Accepted',
|
||||||
@@ -2005,7 +2005,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const statusLabel = statusLabels[opts.status] || opts.status;
|
const statusLabel = statusLabels[opts.status] || opts.status;
|
||||||
const subject = `${statusLabel}: ${opts.summary || 'Event'}`;
|
const subject = `${statusLabel}: ${opts.summary || 'Event'}`;
|
||||||
|
|
||||||
debug.log('[iMIP] identityId:', finalIdentityId);
|
debug.log('calendar', '[iMIP] identityId:', finalIdentityId);
|
||||||
|
|
||||||
const emailId = `imip-reply-${Date.now()}`;
|
const emailId = `imip-reply-${Date.now()}`;
|
||||||
const emailCreate: Record<string, unknown> = {
|
const emailCreate: Record<string, unknown> = {
|
||||||
@@ -2038,12 +2038,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}, "1"],
|
}, "1"],
|
||||||
];
|
];
|
||||||
|
|
||||||
debug.log('[iMIP] Sending JMAP request with', methodCalls.length, 'method calls');
|
debug.log('calendar', '[iMIP] Sending JMAP request with', methodCalls.length, 'method calls');
|
||||||
debug.log('[iMIP] Email create payload:', JSON.stringify(emailCreate, null, 2));
|
debug.log('calendar', '[iMIP] Email create payload:', JSON.stringify(emailCreate, null, 2));
|
||||||
|
|
||||||
const response = await this.request(methodCalls);
|
const response = await this.request(methodCalls);
|
||||||
|
|
||||||
debug.log('[iMIP] JMAP response:', JSON.stringify(response.methodResponses, null, 2));
|
debug.log('calendar', '[iMIP] JMAP response:', JSON.stringify(response.methodResponses, null, 2));
|
||||||
|
|
||||||
if (response.methodResponses) {
|
if (response.methodResponses) {
|
||||||
for (const [methodName, result] of response.methodResponses) {
|
for (const [methodName, result] of response.methodResponses) {
|
||||||
@@ -2058,7 +2058,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug.log('[iMIP] sendImipReply completed successfully');
|
debug.log('calendar', '[iMIP] sendImipReply completed successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2222,7 +2222,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
async sendImipCancellation(event: CalendarEvent): Promise<void> {
|
async sendImipCancellation(event: CalendarEvent): Promise<void> {
|
||||||
if (!event.participants) return;
|
if (!event.participants) return;
|
||||||
if (event.status && event.status !== 'cancelled') {
|
if (event.status && event.status !== 'cancelled') {
|
||||||
debug.warn('sendImipCancellation called on non-cancelled event, status:', event.status);
|
debug.warn('calendar', 'sendImipCancellation called on non-cancelled event, status:', event.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mailboxes = await this.getMailboxes();
|
const mailboxes = await this.getMailboxes();
|
||||||
@@ -3361,8 +3361,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const { originalId: _oi, originalCalendarIds: _oc, accountId: _ai, accountName: _an, isShared: _is, ...cleanEvent } = event as CalendarEvent;
|
const { originalId: _oi, originalCalendarIds: _oc, accountId: _ai, accountName: _an, isShared: _is, ...cleanEvent } = event as CalendarEvent;
|
||||||
cleanRecurrenceRules(cleanEvent as unknown as Record<string, unknown>);
|
cleanRecurrenceRules(cleanEvent as unknown as Record<string, unknown>);
|
||||||
|
|
||||||
debug.group('CalendarEvent/create');
|
debug.group('CalendarEvent/create', 'calendar');
|
||||||
debug.log('CalendarEvent/create outgoing payload', {
|
debug.log('calendar', 'CalendarEvent/create outgoing payload', {
|
||||||
accountId,
|
accountId,
|
||||||
sendSchedulingMessages,
|
sendSchedulingMessages,
|
||||||
eventKeys: Object.keys(cleanEvent),
|
eventKeys: Object.keys(cleanEvent),
|
||||||
@@ -3382,40 +3382,40 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
["CalendarEvent/set", setArgs, "0"]
|
["CalendarEvent/set", setArgs, "0"]
|
||||||
], this.calendarUsing());
|
], this.calendarUsing());
|
||||||
|
|
||||||
debug.log('CalendarEvent/create raw set response', response.methodResponses?.[0]?.[1] || null);
|
debug.log('calendar', 'CalendarEvent/create raw set response', response.methodResponses?.[0]?.[1] || null);
|
||||||
|
|
||||||
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/set") {
|
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/set") {
|
||||||
const result = response.methodResponses[0][1];
|
const result = response.methodResponses[0][1];
|
||||||
|
|
||||||
if (result.notCreated?.["new-event"]) {
|
if (result.notCreated?.["new-event"]) {
|
||||||
const error = result.notCreated["new-event"];
|
const error = result.notCreated["new-event"];
|
||||||
debug.warn('CalendarEvent/create notCreated', error);
|
debug.warn('calendar', 'CalendarEvent/create notCreated', error);
|
||||||
debug.warn('CalendarEvent/create invalid properties', error.properties);
|
debug.warn('calendar', 'CalendarEvent/create invalid properties', error.properties);
|
||||||
debug.warn('CalendarEvent/create sent keys', Object.keys(cleanEvent));
|
debug.warn('calendar', 'CalendarEvent/create sent keys', Object.keys(cleanEvent));
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
throw new Error(error.description || "Failed to create calendar event");
|
throw new Error(error.description || "Failed to create calendar event");
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdId = result.created?.["new-event"]?.id;
|
const createdId = result.created?.["new-event"]?.id;
|
||||||
debug.log('CalendarEvent/create server acknowledged created id', {
|
debug.log('calendar', 'CalendarEvent/create server acknowledged created id', {
|
||||||
createdId,
|
createdId,
|
||||||
created: result.created?.['new-event'] || null,
|
created: result.created?.['new-event'] || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (createdId) {
|
if (createdId) {
|
||||||
const created = await this.getCalendarEvent(createdId, targetAccountId);
|
const created = await this.getCalendarEvent(createdId, targetAccountId);
|
||||||
debug.log('CalendarEvent/create fetched created event', getCalendarEventDebugSnapshot(created));
|
debug.log('calendar', 'CalendarEvent/create fetched created event', getCalendarEventDebugSnapshot(created));
|
||||||
|
|
||||||
if (created?.uid) {
|
if (created?.uid) {
|
||||||
try {
|
try {
|
||||||
const verificationMatches = await this.queryCalendarEvents({ uid: created.uid }, undefined, undefined, targetAccountId);
|
const verificationMatches = await this.queryCalendarEvents({ uid: created.uid }, undefined, undefined, targetAccountId);
|
||||||
debug.log('CalendarEvent/create verification query by uid', {
|
debug.log('calendar', 'CalendarEvent/create verification query by uid', {
|
||||||
uid: created.uid,
|
uid: created.uid,
|
||||||
matchCount: verificationMatches.length,
|
matchCount: verificationMatches.length,
|
||||||
matches: verificationMatches.map((match) => getCalendarEventDebugSnapshot(match)),
|
matches: verificationMatches.map((match) => getCalendarEventDebugSnapshot(match)),
|
||||||
});
|
});
|
||||||
} catch (verificationError) {
|
} catch (verificationError) {
|
||||||
debug.warn('CalendarEvent/create verification query failed', verificationError);
|
debug.warn('calendar', 'CalendarEvent/create verification query failed', verificationError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3424,7 +3424,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.warn('CalendarEvent/create server returned created id but CalendarEvent/get returned null', {
|
debug.warn('calendar', 'CalendarEvent/create server returned created id but CalendarEvent/get returned null', {
|
||||||
createdId,
|
createdId,
|
||||||
targetAccountId,
|
targetAccountId,
|
||||||
});
|
});
|
||||||
@@ -3455,7 +3455,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
createMap[`new-${i}`] = clean;
|
createMap[`new-${i}`] = clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('CalendarEvent/batchCreate', { count: events.length, accountId });
|
debug.log('calendar', 'CalendarEvent/batchCreate', { count: events.length, accountId });
|
||||||
|
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
["CalendarEvent/set", { accountId, create: createMap }, "0"]
|
["CalendarEvent/set", { accountId, create: createMap }, "0"]
|
||||||
@@ -3471,7 +3471,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (result.created?.[key]?.id) {
|
if (result.created?.[key]?.id) {
|
||||||
createdIds.push(result.created[key].id);
|
createdIds.push(result.created[key].id);
|
||||||
} else if (result.notCreated?.[key]) {
|
} else if (result.notCreated?.[key]) {
|
||||||
debug.warn(`CalendarEvent/batchCreate failed for ${key}`, result.notCreated[key]);
|
debug.warn('calendar', `CalendarEvent/batchCreate failed for ${key}`, result.notCreated[key]);
|
||||||
failed.push(key);
|
failed.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3496,7 +3496,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
createdEvents = list.map((e: CalendarEvent) => normalizeCalendarEventLike(e));
|
createdEvents = list.map((e: CalendarEvent) => normalizeCalendarEventLike(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('CalendarEvent/batchCreate result', {
|
debug.log('calendar', 'CalendarEvent/batchCreate result', {
|
||||||
requested: events.length,
|
requested: events.length,
|
||||||
created: createdEvents.length,
|
created: createdEvents.length,
|
||||||
failed: failed.length,
|
failed: failed.length,
|
||||||
@@ -3527,7 +3527,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('CalendarEvent/set update request', { eventId, accountId, cleanUpdateKeys: Object.keys(cleanUpdates), sendSchedulingMessages });
|
debug.log('calendar', 'CalendarEvent/set update request', { eventId, accountId, cleanUpdateKeys: Object.keys(cleanUpdates), sendSchedulingMessages });
|
||||||
|
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
["CalendarEvent/set", setArgs, "0"]
|
["CalendarEvent/set", setArgs, "0"]
|
||||||
@@ -3549,7 +3549,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
debug.error('CalendarEvent/set notUpdated', { eventId, error });
|
debug.error('CalendarEvent/set notUpdated', { eventId, error });
|
||||||
throw new Error(error.description || "Failed to update calendar event");
|
throw new Error(error.description || "Failed to update calendar event");
|
||||||
}
|
}
|
||||||
debug.log('CalendarEvent/set update success', { eventId, updated: result.updated ? Object.keys(result.updated) : null });
|
debug.log('calendar', 'CalendarEvent/set update success', { eventId, updated: result.updated ? Object.keys(result.updated) : null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3600,7 +3600,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('CalendarEvent/set destroy request', { eventId, accountId, sendSchedulingMessages });
|
debug.log('calendar', 'CalendarEvent/set destroy request', { eventId, accountId, sendSchedulingMessages });
|
||||||
|
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
["CalendarEvent/set", setArgs, "0"]
|
["CalendarEvent/set", setArgs, "0"]
|
||||||
@@ -3622,7 +3622,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
debug.error('CalendarEvent/set notDestroyed', { eventId, error });
|
debug.error('CalendarEvent/set notDestroyed', { eventId, error });
|
||||||
throw new Error(error.description || "Failed to delete calendar event");
|
throw new Error(error.description || "Failed to delete calendar event");
|
||||||
}
|
}
|
||||||
debug.log('CalendarEvent/set destroy success', { eventId, destroyed: result.destroyed });
|
debug.log('calendar', 'CalendarEvent/set destroy success', { eventId, destroyed: result.destroyed });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3654,8 +3654,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
async getCalendarTasks(calendarIds?: string[], targetAccountId?: string): Promise<CalendarTask[]> {
|
async getCalendarTasks(calendarIds?: string[], targetAccountId?: string): Promise<CalendarTask[]> {
|
||||||
const accountId = targetAccountId || this.getCalendarsAccountId();
|
const accountId = targetAccountId || this.getCalendarsAccountId();
|
||||||
debug.group('CalendarTask/fetch');
|
debug.group('CalendarTask/fetch', 'tasks');
|
||||||
debug.log('CalendarTask/fetch start', { accountId, calendarIds: calendarIds || 'all' });
|
debug.log('tasks', 'CalendarTask/fetch start', { accountId, calendarIds: calendarIds || 'all' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Strategy 1: query with types filter (JMAP spec compliant)
|
// Strategy 1: query with types filter (JMAP spec compliant)
|
||||||
@@ -3664,7 +3664,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
filter.inCalendars = calendarIds;
|
filter.inCalendars = calendarIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('CalendarTask/fetch query filter', filter);
|
debug.log('tasks', 'CalendarTask/fetch query filter', filter);
|
||||||
|
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
["CalendarEvent/query", { accountId, filter, limit: 1000 }, "0"],
|
["CalendarEvent/query", { accountId, filter, limit: 1000 }, "0"],
|
||||||
@@ -3678,13 +3678,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const queryResponse = response.methodResponses?.[0];
|
const queryResponse = response.methodResponses?.[0];
|
||||||
const getResponse = response.methodResponses?.[1];
|
const getResponse = response.methodResponses?.[1];
|
||||||
|
|
||||||
debug.log('CalendarTask/fetch query method', queryResponse?.[0]);
|
debug.log('tasks', 'CalendarTask/fetch query method', queryResponse?.[0]);
|
||||||
debug.log('CalendarTask/fetch query result', queryResponse?.[1]);
|
debug.log('tasks', 'CalendarTask/fetch query result', queryResponse?.[1]);
|
||||||
|
|
||||||
if (queryResponse?.[0] === "error") {
|
if (queryResponse?.[0] === "error") {
|
||||||
debug.warn('CalendarTask/fetch types filter not supported, falling back to full scan', queryResponse[1]);
|
debug.warn('tasks', 'CalendarTask/fetch types filter not supported, falling back to full scan', queryResponse[1]);
|
||||||
const tasks = await this.getCalendarTasksFallback(calendarIds, targetAccountId);
|
const tasks = await this.getCalendarTasksFallback(calendarIds, targetAccountId);
|
||||||
debug.log('CalendarTask/fetch fallback returned', tasks.length, 'tasks');
|
debug.log('tasks', 'CalendarTask/fetch fallback returned', tasks.length, 'tasks');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
@@ -3692,22 +3692,22 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (getResponse?.[0] === "CalendarEvent/get") {
|
if (getResponse?.[0] === "CalendarEvent/get") {
|
||||||
const list = (getResponse[1].list || []) as CalendarTask[];
|
const list = (getResponse[1].list || []) as CalendarTask[];
|
||||||
const queryIds = queryResponse?.[1]?.ids || [];
|
const queryIds = queryResponse?.[1]?.ids || [];
|
||||||
debug.log('CalendarTask/fetch query returned', queryIds.length, 'ids:', queryIds);
|
debug.log('calendar', 'CalendarTask/fetch query returned', queryIds.length, 'ids:', queryIds);
|
||||||
debug.log('CalendarTask/fetch get returned', list.length, 'objects');
|
debug.log('calendar', 'CalendarTask/fetch get returned', list.length, 'objects');
|
||||||
|
|
||||||
// If the types filter returned 0 results, the server may have silently
|
// If the types filter returned 0 results, the server may have silently
|
||||||
// ignored it (e.g. Stalwart with CalDAV-created VTODOs). Fall back to
|
// ignored it (e.g. Stalwart with CalDAV-created VTODOs). Fall back to
|
||||||
// a full scan so we can detect tasks by their properties.
|
// a full scan so we can detect tasks by their properties.
|
||||||
if (queryIds.length === 0) {
|
if (queryIds.length === 0) {
|
||||||
debug.warn('CalendarTask/fetch types filter returned 0 results, falling back to full scan');
|
debug.warn('tasks', 'CalendarTask/fetch types filter returned 0 results, falling back to full scan');
|
||||||
const tasks = await this.getCalendarTasksFallback(calendarIds, targetAccountId);
|
const tasks = await this.getCalendarTasksFallback(calendarIds, targetAccountId);
|
||||||
debug.log('CalendarTask/fetch fallback returned', tasks.length, 'tasks');
|
debug.log('tasks', 'CalendarTask/fetch fallback returned', tasks.length, 'tasks');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.forEach((task, i) => {
|
list.forEach((task, i) => {
|
||||||
debug.log(`CalendarTask/fetch [${i}]`, {
|
debug.log('tasks', `CalendarTask/fetch [${i}]`, {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
uid: task.uid,
|
uid: task.uid,
|
||||||
'@type': task['@type'],
|
'@type': task['@type'],
|
||||||
@@ -3724,12 +3724,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
...task,
|
...task,
|
||||||
'@type': 'Task' as const,
|
'@type': 'Task' as const,
|
||||||
}));
|
}));
|
||||||
debug.log('CalendarTask/fetch complete,', results.length, 'tasks');
|
debug.log('tasks', 'CalendarTask/fetch complete,', results.length, 'tasks');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.warn('CalendarTask/fetch unexpected response shape', response.methodResponses);
|
debug.warn('tasks', 'CalendarTask/fetch unexpected response shape', response.methodResponses);
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3746,7 +3746,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
*/
|
*/
|
||||||
private async getCalendarTasksFallback(calendarIds?: string[], targetAccountId?: string): Promise<CalendarTask[]> {
|
private async getCalendarTasksFallback(calendarIds?: string[], targetAccountId?: string): Promise<CalendarTask[]> {
|
||||||
const accountId = targetAccountId || this.getCalendarsAccountId();
|
const accountId = targetAccountId || this.getCalendarsAccountId();
|
||||||
debug.log('CalendarTask/fallback using CalendarEvent/get ids:null to fetch all objects');
|
debug.log('calendar', 'CalendarTask/fallback using CalendarEvent/get ids:null to fetch all objects');
|
||||||
|
|
||||||
// CalendarEvent/get with ids:null returns ALL calendar objects regardless of @type
|
// CalendarEvent/get with ids:null returns ALL calendar objects regardless of @type
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
@@ -3758,12 +3758,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
], this.calendarUsing());
|
], this.calendarUsing());
|
||||||
|
|
||||||
if (response.methodResponses?.[0]?.[0] !== "CalendarEvent/get") {
|
if (response.methodResponses?.[0]?.[0] !== "CalendarEvent/get") {
|
||||||
debug.warn('CalendarTask/fallback unexpected response', response.methodResponses?.[0]);
|
debug.warn('calendar', 'CalendarTask/fallback unexpected response', response.methodResponses?.[0]);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allObjects = (response.methodResponses[0][1].list || []) as Record<string, unknown>[];
|
const allObjects = (response.methodResponses[0][1].list || []) as Record<string, unknown>[];
|
||||||
debug.log('CalendarTask/fallback total calendar objects returned:', allObjects.length);
|
debug.log('tasks', 'CalendarTask/fallback total calendar objects returned:', allObjects.length);
|
||||||
|
|
||||||
const tasks: CalendarTask[] = [];
|
const tasks: CalendarTask[] = [];
|
||||||
const calendarIdSet = calendarIds ? new Set(calendarIds) : null;
|
const calendarIdSet = calendarIds ? new Set(calendarIds) : null;
|
||||||
@@ -3779,7 +3779,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|| ('percentComplete' in obj);
|
|| ('percentComplete' in obj);
|
||||||
const isCalDavTask = type !== 'Event' && hasTaskFields;
|
const isCalDavTask = type !== 'Event' && hasTaskFields;
|
||||||
|
|
||||||
debug.log('CalendarTask/fallback scan', {
|
debug.log('tasks', 'CalendarTask/fallback scan', {
|
||||||
id: obj.id,
|
id: obj.id,
|
||||||
'@type': type,
|
'@type': type,
|
||||||
title: obj.title,
|
title: obj.title,
|
||||||
@@ -3796,7 +3796,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (calendarIdSet) {
|
if (calendarIdSet) {
|
||||||
const objCalendarIds = obj.calendarIds as Record<string, boolean> | undefined;
|
const objCalendarIds = obj.calendarIds as Record<string, boolean> | undefined;
|
||||||
if (objCalendarIds && !Object.keys(objCalendarIds).some(id => calendarIdSet.has(id))) {
|
if (objCalendarIds && !Object.keys(objCalendarIds).some(id => calendarIdSet.has(id))) {
|
||||||
debug.log('CalendarTask/fallback skipping task (not in requested calendars)', obj.id);
|
debug.log('tasks', 'CalendarTask/fallback skipping task (not in requested calendars)', obj.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3804,9 +3804,9 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
tasks.push({ ...obj, '@type': 'Task' as const } as CalendarTask);
|
tasks.push({ ...obj, '@type': 'Task' as const } as CalendarTask);
|
||||||
});
|
});
|
||||||
|
|
||||||
debug.log('CalendarTask/fallback detected', tasks.length, 'tasks');
|
debug.log('tasks', 'CalendarTask/fallback detected', tasks.length, 'tasks');
|
||||||
tasks.forEach((t, i) => {
|
tasks.forEach((t, i) => {
|
||||||
debug.log(`CalendarTask/fallback [${i}]`, {
|
debug.log('tasks', `CalendarTask/fallback [${i}]`, {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
uid: t.uid,
|
uid: t.uid,
|
||||||
title: t.title,
|
title: t.title,
|
||||||
@@ -3825,9 +3825,9 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const { '@type': _type, ...taskData } = task;
|
const { '@type': _type, ...taskData } = task;
|
||||||
const cleanTask = { ...taskData, '@type': 'Task' };
|
const cleanTask = { ...taskData, '@type': 'Task' };
|
||||||
|
|
||||||
debug.group('CalendarTask/create');
|
debug.group('CalendarTask/create', 'tasks');
|
||||||
debug.log('CalendarTask/create accountId', accountId);
|
debug.log('tasks', 'CalendarTask/create accountId', accountId);
|
||||||
debug.log('CalendarTask/create outgoing payload', cleanTask);
|
debug.log('tasks', 'CalendarTask/create outgoing payload', cleanTask);
|
||||||
|
|
||||||
const response = await this.request([
|
const response = await this.request([
|
||||||
["CalendarEvent/set", {
|
["CalendarEvent/set", {
|
||||||
@@ -3838,27 +3838,27 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
], this.calendarUsing());
|
], this.calendarUsing());
|
||||||
|
|
||||||
const result = response.methodResponses?.[0]?.[1];
|
const result = response.methodResponses?.[0]?.[1];
|
||||||
debug.log('CalendarTask/create raw set response', result);
|
debug.log('tasks', 'CalendarTask/create raw set response', result);
|
||||||
|
|
||||||
if (result?.notCreated?.["new-task"]) {
|
if (result?.notCreated?.["new-task"]) {
|
||||||
const error = result.notCreated["new-task"];
|
const error = result.notCreated["new-task"];
|
||||||
debug.warn('CalendarTask/create REJECTED by server', error);
|
debug.warn('tasks', 'CalendarTask/create REJECTED by server', error);
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
throw new Error(error.description || "Failed to create task");
|
throw new Error(error.description || "Failed to create task");
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdId = result?.created?.["new-task"]?.id;
|
const createdId = result?.created?.["new-task"]?.id;
|
||||||
const serverCreated = result?.created?.["new-task"];
|
const serverCreated = result?.created?.["new-task"];
|
||||||
debug.log('CalendarTask/create server acknowledged', { createdId, serverCreated });
|
debug.log('tasks', 'CalendarTask/create server acknowledged', { createdId, serverCreated });
|
||||||
|
|
||||||
if (!createdId) {
|
if (!createdId) {
|
||||||
debug.warn('CalendarTask/create no id in server response');
|
debug.warn('tasks', 'CalendarTask/create no id in server response');
|
||||||
debug.groupEnd();
|
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
|
// Fetch back with task-specific properties
|
||||||
debug.log('CalendarTask/create re-fetching with task properties', { createdId, properties: [...CALENDAR_TASK_PROPERTIES] });
|
debug.log('calendar', 'CalendarTask/create re-fetching with task properties', { createdId, properties: [...CALENDAR_TASK_PROPERTIES] });
|
||||||
const getResponse = await this.request([
|
const getResponse = await this.request([
|
||||||
["CalendarEvent/get", {
|
["CalendarEvent/get", {
|
||||||
accountId,
|
accountId,
|
||||||
@@ -3870,10 +3870,10 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (getResponse.methodResponses?.[0]?.[0] === "CalendarEvent/get") {
|
if (getResponse.methodResponses?.[0]?.[0] === "CalendarEvent/get") {
|
||||||
const list = getResponse.methodResponses[0][1].list || [];
|
const list = getResponse.methodResponses[0][1].list || [];
|
||||||
const notFound = getResponse.methodResponses[0][1].notFound || [];
|
const notFound = getResponse.methodResponses[0][1].notFound || [];
|
||||||
debug.log('CalendarTask/create get response', { found: list.length, notFound });
|
debug.log('calendar', 'CalendarTask/create get response', { found: list.length, notFound });
|
||||||
if (list[0]) {
|
if (list[0]) {
|
||||||
const created = { ...list[0], '@type': 'Task' as const } as CalendarTask;
|
const created = { ...list[0], '@type': 'Task' as const } as CalendarTask;
|
||||||
debug.log('CalendarTask/create final task object', {
|
debug.log('tasks', 'CalendarTask/create final task object', {
|
||||||
id: created.id,
|
id: created.id,
|
||||||
uid: created.uid,
|
uid: created.uid,
|
||||||
'@type': created['@type'],
|
'@type': created['@type'],
|
||||||
@@ -3889,7 +3889,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.warn('CalendarTask/create re-fetch returned nothing for id', createdId);
|
debug.warn('tasks', 'CalendarTask/create re-fetch returned nothing for id', createdId);
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
throw new Error("Failed to fetch created task");
|
throw new Error("Failed to fetch created task");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function playFile(file: string) {
|
|||||||
const audio = new Audio(file);
|
const audio = new Audio(file);
|
||||||
audio.volume = 0.3;
|
audio.volume = 0.3;
|
||||||
audio.play().catch((e) => {
|
audio.play().catch((e) => {
|
||||||
debug.log('Could not play audio file, falling back to beep:', e);
|
debug.log('push', 'Could not play audio file, falling back to beep:', e);
|
||||||
playBeep();
|
playBeep();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,6 @@ export function playNotificationSound(sound?: NotificationSoundChoice) {
|
|||||||
playBeep();
|
playBeep();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug.log('Could not play notification sound:', e);
|
debug.log('push', 'Could not play notification sound:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function generateScript(rules: FilterRule[], vacation?: VacationSieveConf
|
|||||||
|
|
||||||
for (const rule of enabledRules) {
|
for (const rule of enabledRules) {
|
||||||
if (rule.conditions.length === 0 || rule.actions.length === 0) {
|
if (rule.conditions.length === 0 || rule.actions.length === 0) {
|
||||||
debug.warn(`Skipping rule "${rule.name}": empty conditions or actions`);
|
debug.warn('filters', `Skipping rule "${rule.name}": empty conditions or actions`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ export function parseScript(content: string): ParseResult {
|
|||||||
try {
|
try {
|
||||||
metadata = JSON.parse(jsonStr);
|
metadata = JSON.parse(jsonStr);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug.warn('Failed to parse Sieve metadata JSON:', e);
|
debug.warn('filters', 'Failed to parse Sieve metadata JSON:', e);
|
||||||
return OPAQUE;
|
return OPAQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-27
@@ -236,11 +236,11 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
debug.group('TNEF Parser');
|
debug.group('TNEF Parser', 'email');
|
||||||
debug.log('Input data size:', data.byteLength, 'bytes');
|
debug.log('email', 'Input data size:', data.byteLength, 'bytes');
|
||||||
|
|
||||||
if (data.byteLength < 6) {
|
if (data.byteLength < 6) {
|
||||||
debug.warn('TNEF data too small (< 6 bytes), skipping');
|
debug.warn('email', 'TNEF data too small (< 6 bytes), skipping');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -249,11 +249,11 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
|
|
||||||
const signature = r.readUint32LE();
|
const signature = r.readUint32LE();
|
||||||
if (signature !== TNEF_SIGNATURE) {
|
if (signature !== TNEF_SIGNATURE) {
|
||||||
debug.warn('Invalid TNEF signature:', '0x' + signature.toString(16).toUpperCase(), '(expected 0x223E9F78)');
|
debug.warn('email', 'Invalid TNEF signature:', '0x' + signature.toString(16).toUpperCase(), '(expected 0x223E9F78)');
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
debug.log('TNEF signature valid');
|
debug.log('email', 'TNEF signature valid');
|
||||||
|
|
||||||
r.skip(2); // legacy key
|
r.skip(2); // legacy key
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
attrCount++;
|
attrCount++;
|
||||||
|
|
||||||
if (attrLen > r.remaining - 2) {
|
if (attrLen > r.remaining - 2) {
|
||||||
debug.warn('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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,17 +276,17 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
r.skip(2); // checksum
|
r.skip(2); // checksum
|
||||||
|
|
||||||
const levelName = level === LVL_MESSAGE ? 'MESSAGE' : level === LVL_ATTACHMENT ? 'ATTACHMENT' : 'UNKNOWN(' + level + ')';
|
const levelName = level === LVL_MESSAGE ? 'MESSAGE' : level === LVL_ATTACHMENT ? 'ATTACHMENT' : 'UNKNOWN(' + level + ')';
|
||||||
debug.log('Attribute #' + attrCount + ':', levelName, 'id=0x' + attrID.toString(16).toUpperCase(), 'len=' + attrLen);
|
debug.log('email', 'Attribute #' + attrCount + ':', levelName, 'id=0x' + attrID.toString(16).toUpperCase(), 'len=' + attrLen);
|
||||||
|
|
||||||
if (level === LVL_MESSAGE) {
|
if (level === LVL_MESSAGE) {
|
||||||
if (attrID === attBody) {
|
if (attrID === attBody) {
|
||||||
result.body = new TextDecoder('utf-8').decode(attrData);
|
result.body = new TextDecoder('utf-8').decode(attrData);
|
||||||
debug.log(' → Extracted plain text body (' + result.body.length + ' chars)');
|
debug.log('email', ' → Extracted plain text body (' + result.body.length + ' chars)');
|
||||||
} else if (attrID === attMAPIProps) {
|
} else if (attrID === attMAPIProps) {
|
||||||
const props = parseMAPIProps(attrData);
|
const props = parseMAPIProps(attrData);
|
||||||
debug.log(' → Parsed', props.size, 'MAPI properties from message');
|
debug.log('email', ' → Parsed', props.size, 'MAPI properties from message');
|
||||||
props.forEach((val, propID) => {
|
props.forEach((val, propID) => {
|
||||||
debug.log(' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
|
debug.log('email', ' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTML body
|
// HTML body
|
||||||
@@ -298,9 +298,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
} else {
|
} else {
|
||||||
result.htmlBody = new TextDecoder('utf-8').decode(htmlProp.value);
|
result.htmlBody = new TextDecoder('utf-8').decode(htmlProp.value);
|
||||||
}
|
}
|
||||||
debug.log(' → Extracted HTML body (' + result.htmlBody.length + ' chars)');
|
debug.log('email', ' → Extracted HTML body (' + result.htmlBody.length + ' chars)');
|
||||||
} else {
|
} else {
|
||||||
debug.log(' → No HTML body property (PR_BODY_HTML 0x1013) found in MAPI props');
|
debug.log('email', ' → No HTML body property (PR_BODY_HTML 0x1013) found in MAPI props');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain text body from MAPI props (fallback)
|
// Plain text body from MAPI props (fallback)
|
||||||
@@ -308,9 +308,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
const bodyProp = props.get(PR_BODY);
|
const bodyProp = props.get(PR_BODY);
|
||||||
if (bodyProp?.value instanceof Uint8Array) {
|
if (bodyProp?.value instanceof Uint8Array) {
|
||||||
result.body = decodeMAPIString(bodyProp.value, bodyProp.type & 0x0FFF);
|
result.body = decodeMAPIString(bodyProp.value, bodyProp.type & 0x0FFF);
|
||||||
debug.log(' → Extracted plain text body from MAPI props (' + result.body.length + ' chars)');
|
debug.log('email', ' → Extracted plain text body from MAPI props (' + result.body.length + ' chars)');
|
||||||
} else {
|
} else {
|
||||||
debug.log(' → No plain text body property (PR_BODY 0x1000) found in MAPI props');
|
debug.log('email', ' → No plain text body property (PR_BODY 0x1000) found in MAPI props');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
if (attrID === attAttachRenddata) {
|
if (attrID === attAttachRenddata) {
|
||||||
// Start of a new attachment — flush previous
|
// Start of a new attachment — flush previous
|
||||||
if (curAttach?.data) {
|
if (curAttach?.data) {
|
||||||
debug.log(' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
debug.log('email', ' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
||||||
result.attachments.push({
|
result.attachments.push({
|
||||||
name: curAttach.name,
|
name: curAttach.name,
|
||||||
mimeType: curAttach.mimeType,
|
mimeType: curAttach.mimeType,
|
||||||
@@ -326,40 +326,40 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null };
|
curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null };
|
||||||
debug.log(' → New attachment started');
|
debug.log('email', ' → New attachment started');
|
||||||
} else if (attrID === attAttachTitle && curAttach) {
|
} else if (attrID === attAttachTitle && curAttach) {
|
||||||
let len = attrData.byteLength;
|
let len = attrData.byteLength;
|
||||||
if (len > 0 && attrData[len - 1] === 0) len--;
|
if (len > 0 && attrData[len - 1] === 0) len--;
|
||||||
curAttach.name = new TextDecoder('utf-8').decode(attrData.subarray(0, len));
|
curAttach.name = new TextDecoder('utf-8').decode(attrData.subarray(0, len));
|
||||||
debug.log(' → Attachment short name:', curAttach.name);
|
debug.log('email', ' → Attachment short name:', curAttach.name);
|
||||||
} else if (attrID === attAttachData && curAttach) {
|
} else if (attrID === attAttachData && curAttach) {
|
||||||
curAttach.data = attrData;
|
curAttach.data = attrData;
|
||||||
debug.log(' → Attachment data (attAttachData):', attrData.byteLength, 'bytes');
|
debug.log('email', ' → Attachment data (attAttachData):', attrData.byteLength, 'bytes');
|
||||||
} else if (attrID === attAttachment && curAttach) {
|
} else if (attrID === attAttachment && curAttach) {
|
||||||
const props = parseMAPIProps(attrData);
|
const props = parseMAPIProps(attrData);
|
||||||
debug.log(' → Parsed', props.size, 'MAPI properties from attachment');
|
debug.log('email', ' → Parsed', props.size, 'MAPI properties from attachment');
|
||||||
props.forEach((val, propID) => {
|
props.forEach((val, propID) => {
|
||||||
debug.log(' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
|
debug.log('email', ' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
const longName = props.get(PR_ATTACH_LONG_FILENAME);
|
const longName = props.get(PR_ATTACH_LONG_FILENAME);
|
||||||
if (longName?.value instanceof Uint8Array) {
|
if (longName?.value instanceof Uint8Array) {
|
||||||
curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF);
|
curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF);
|
||||||
debug.log(' → Attachment long filename:', curAttach.name);
|
debug.log('email', ' → Attachment long filename:', curAttach.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeTag = props.get(PR_ATTACH_MIME_TAG);
|
const mimeTag = props.get(PR_ATTACH_MIME_TAG);
|
||||||
if (mimeTag?.value instanceof Uint8Array) {
|
if (mimeTag?.value instanceof Uint8Array) {
|
||||||
curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF);
|
curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF);
|
||||||
debug.log(' → Attachment MIME type:', curAttach.mimeType);
|
debug.log('email', ' → Attachment MIME type:', curAttach.mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachData = props.get(PR_ATTACH_DATA_BIN);
|
const attachData = props.get(PR_ATTACH_DATA_BIN);
|
||||||
if (attachData?.value instanceof Uint8Array) {
|
if (attachData?.value instanceof Uint8Array) {
|
||||||
curAttach.data = attachData.value;
|
curAttach.data = attachData.value;
|
||||||
debug.log(' → Attachment data (PR_ATTACH_DATA_BIN):', attachData.value.byteLength, 'bytes');
|
debug.log('email', ' → Attachment data (PR_ATTACH_DATA_BIN):', attachData.value.byteLength, 'bytes');
|
||||||
} else {
|
} else {
|
||||||
debug.log(' → No PR_ATTACH_DATA_BIN found in attachment MAPI props');
|
debug.log('email', ' → No PR_ATTACH_DATA_BIN found in attachment MAPI props');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
|
|
||||||
// Flush last attachment
|
// Flush last attachment
|
||||||
if (curAttach?.data) {
|
if (curAttach?.data) {
|
||||||
debug.log('Flushing final attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
debug.log('email', 'Flushing final attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
||||||
result.attachments.push({
|
result.attachments.push({
|
||||||
name: curAttach.name,
|
name: curAttach.name,
|
||||||
mimeType: curAttach.mimeType,
|
mimeType: curAttach.mimeType,
|
||||||
@@ -375,9 +375,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('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) {
|
if (result.attachments.length > 0) {
|
||||||
debug.table(result.attachments.map(a => ({ name: a.name, mimeType: a.mimeType, size: a.data.byteLength })));
|
debug.table(result.attachments.map(a => ({ name: a.name, mimeType: a.mimeType, size: a.data.byteLength })), 'email');
|
||||||
}
|
}
|
||||||
debug.groupEnd();
|
debug.groupEnd();
|
||||||
|
|
||||||
|
|||||||
+6
-9
@@ -152,8 +152,7 @@ function deduplicateMailboxes(mailboxes: Mailbox[]): Mailbox[] {
|
|||||||
removed.push({ id: mb.id, name: mb.name, matchedRole: matchedRole!.name, parentId: mb.parentId });
|
removed.push({ id: mb.id, name: mb.name, matchedRole: matchedRole!.name, parentId: mb.parentId });
|
||||||
// Warn if this removed mailbox is a parent of other mailboxes (orphan risk)
|
// Warn if this removed mailbox is a parent of other mailboxes (orphan risk)
|
||||||
if (referencedParentIds.has(mb.id)) {
|
if (referencedParentIds.has(mb.id)) {
|
||||||
debug.warn(
|
debug.warn('jmap', `[Mailbox Tree] Deduplication removed mailbox "${mb.name}" (id: ${mb.id}) which is a parent of other mailboxes. ` +
|
||||||
`[Mailbox Tree] Deduplication removed mailbox "${mb.name}" (id: ${mb.id}) which is a parent of other mailboxes. ` +
|
|
||||||
`Matched role mailbox: "${matchedRole!.name}" (role: ${matchedRole!.role}). ` +
|
`Matched role mailbox: "${matchedRole!.name}" (role: ${matchedRole!.role}). ` +
|
||||||
`Children referencing parentId "${mb.id}" will be orphaned to root level.`
|
`Children referencing parentId "${mb.id}" will be orphaned to root level.`
|
||||||
);
|
);
|
||||||
@@ -162,7 +161,7 @@ function deduplicateMailboxes(mailboxes: Mailbox[]): Mailbox[] {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
debug.log(`[Mailbox Tree] Deduplication removed ${removed.length} mailbox(es):`, removed);
|
debug.log('jmap', `[Mailbox Tree] Deduplication removed ${removed.length} mailbox(es):`, removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -170,13 +169,13 @@ function deduplicateMailboxes(mailboxes: Mailbox[]): Mailbox[] {
|
|||||||
|
|
||||||
// Build a hierarchical tree structure from flat mailbox array
|
// Build a hierarchical tree structure from flat mailbox array
|
||||||
export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
|
export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
|
||||||
debug.log(`[Mailbox Tree] Building tree from ${mailboxes.length} mailboxes`);
|
debug.log('jmap', `[Mailbox Tree] Building tree from ${mailboxes.length} mailboxes`);
|
||||||
|
|
||||||
// Deduplicate mailboxes first
|
// Deduplicate mailboxes first
|
||||||
const deduplicated = deduplicateMailboxes(mailboxes);
|
const deduplicated = deduplicateMailboxes(mailboxes);
|
||||||
|
|
||||||
if (deduplicated.length !== mailboxes.length) {
|
if (deduplicated.length !== mailboxes.length) {
|
||||||
debug.log(`[Mailbox Tree] After deduplication: ${deduplicated.length} mailboxes (removed ${mailboxes.length - deduplicated.length})`);
|
debug.log('jmap', `[Mailbox Tree] After deduplication: ${deduplicated.length} mailboxes (removed ${mailboxes.length - deduplicated.length})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate own and shared mailboxes
|
// Separate own and shared mailboxes
|
||||||
@@ -223,8 +222,7 @@ export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (orphanedMailboxes.length > 0) {
|
if (orphanedMailboxes.length > 0) {
|
||||||
debug.warn(
|
debug.warn('jmap', `[Mailbox Tree] ${orphanedMailboxes.length} orphaned mailbox(es) moved to root level (missing parent):`,
|
||||||
`[Mailbox Tree] ${orphanedMailboxes.length} orphaned mailbox(es) moved to root level (missing parent):`,
|
|
||||||
orphanedMailboxes
|
orphanedMailboxes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -241,8 +239,7 @@ export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
|
|||||||
}
|
}
|
||||||
return max;
|
return max;
|
||||||
};
|
};
|
||||||
debug.log(
|
debug.log('jmap', `[Mailbox Tree] Built tree: ${rootMailboxes.length} root nodes, ` +
|
||||||
`[Mailbox Tree] Built tree: ${rootMailboxes.length} root nodes, ` +
|
|
||||||
`max depth: ${maxDepth(rootMailboxes)}, ` +
|
`max depth: ${maxDepth(rootMailboxes)}, ` +
|
||||||
`total own: ${ownMailboxes.length}, shared: ${sharedMailboxes.length}`
|
`total own: ${ownMailboxes.length}, shared: ${sharedMailboxes.length}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1164,6 +1164,23 @@
|
|||||||
"label": "Debug Mode",
|
"label": "Debug Mode",
|
||||||
"description": "Enable detailed logging for troubleshooting"
|
"description": "Enable detailed logging for troubleshooting"
|
||||||
},
|
},
|
||||||
|
"debug_categories": {
|
||||||
|
"description": "Select which categories to log. Disable categories you don't need to reduce console noise.",
|
||||||
|
"jmap": "JMAP Client",
|
||||||
|
"jmap_description": "Mailbox operations, email fetching, and JMAP protocol requests",
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"calendar_description": "Calendar events, imports, and scheduling messages",
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"tasks_description": "Calendar task creation, fetching, and updates",
|
||||||
|
"auth": "Authentication",
|
||||||
|
"auth_description": "Login, TOTP, token exchange, and session management",
|
||||||
|
"filters": "Filters",
|
||||||
|
"filters_description": "Sieve filter rules and vacation scripts",
|
||||||
|
"email": "Email Viewing",
|
||||||
|
"email_description": "Email rendering, TNEF processing, and mark-as-read",
|
||||||
|
"push": "Push Notifications",
|
||||||
|
"push_description": "Push notification setup and delivery"
|
||||||
|
},
|
||||||
"settings_sync": {
|
"settings_sync": {
|
||||||
"label": "Settings Sync",
|
"label": "Settings Sync",
|
||||||
"description": "Sync your settings across browsers and devices"
|
"description": "Sync your settings across browsers and devices"
|
||||||
|
|||||||
@@ -393,13 +393,13 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
oauthAccessToken = access_token;
|
oauthAccessToken = access_token;
|
||||||
oauthExpiresIn = expires_in;
|
oauthExpiresIn = expires_in;
|
||||||
upgradedToOAuth = true;
|
upgradedToOAuth = true;
|
||||||
debug.log('TOTP login upgraded to token-based auth (has_refresh_token=' + has_refresh_token + ')');
|
debug.log('auth', 'TOTP login upgraded to token-based auth (has_refresh_token=' + has_refresh_token + ')');
|
||||||
} else {
|
} else {
|
||||||
const errorBody = await tokenRes.json().catch(() => ({ error: 'unknown' }));
|
const errorBody = await tokenRes.json().catch(() => ({ error: 'unknown' }));
|
||||||
debug.warn('TOTP token exchange failed:', tokenRes.status, errorBody);
|
debug.warn('auth', 'TOTP token exchange failed:', tokenRes.status, errorBody);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug.warn('TOTP token exchange error:', err);
|
debug.warn('auth', 'TOTP token exchange error:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If token exchange failed, enable TOTP re-auth prompt so the
|
// If token exchange failed, enable TOTP re-auth prompt so the
|
||||||
@@ -407,7 +407,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
if (!upgradedToOAuth) {
|
if (!upgradedToOAuth) {
|
||||||
const { useTotpReauthStore } = await import('@/stores/totp-reauth-store');
|
const { useTotpReauthStore } = await import('@/stores/totp-reauth-store');
|
||||||
client.enableTotpReauth(password, () => useTotpReauthStore.getState().requestTotp());
|
client.enableTotpReauth(password, () => useTotpReauthStore.getState().requestTotp());
|
||||||
debug.log('TOTP re-auth enabled — user will be prompted for fresh codes on session expiry');
|
debug.log('auth', 'TOTP re-auth enabled — user will be prompted for fresh codes on session expiry');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-17
@@ -197,7 +197,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
// Expand recurring events client-side (Stalwart doesn't support
|
// Expand recurring events client-side (Stalwart doesn't support
|
||||||
// mutations on synthetic IDs from server-side expandRecurrences)
|
// mutations on synthetic IDs from server-side expandRecurrences)
|
||||||
const events = expandRecurringEvents(validEvents, start, end);
|
const events = expandRecurringEvents(validEvents, start, end);
|
||||||
debug.log('Calendar fetchEvents completed', {
|
debug.log('calendar', 'Calendar fetchEvents completed', {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
rawCount: rawEvents.length,
|
rawCount: rawEvents.length,
|
||||||
@@ -206,7 +206,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
droppedEvents,
|
droppedEvents,
|
||||||
});
|
});
|
||||||
if (droppedEvents > 0) {
|
if (droppedEvents > 0) {
|
||||||
debug.warn('Calendar fetchEvents dropped malformed events without a start field', { droppedEvents });
|
debug.warn('calendar', 'Calendar fetchEvents dropped malformed events without a start field', { droppedEvents });
|
||||||
}
|
}
|
||||||
set({ events, isLoadingEvents: false, dateRange: { start, end } });
|
set({ events, isLoadingEvents: false, dateRange: { start, end } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -237,7 +237,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
if (event.originalCalendarIds) {
|
if (event.originalCalendarIds) {
|
||||||
cleanEvent.calendarIds = event.originalCalendarIds;
|
cleanEvent.calendarIds = event.originalCalendarIds;
|
||||||
}
|
}
|
||||||
debug.log('Calendar createEvent request', {
|
debug.log('calendar', 'Calendar createEvent request', {
|
||||||
event: getStoreEventDebugSnapshot(cleanEvent),
|
event: getStoreEventDebugSnapshot(cleanEvent),
|
||||||
sendSchedulingMessages,
|
sendSchedulingMessages,
|
||||||
targetAccountId,
|
targetAccountId,
|
||||||
@@ -256,7 +256,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
? mappedCreated.start >= currentDateRange.start && mappedCreated.start <= currentDateRange.end
|
? mappedCreated.start >= currentDateRange.start && mappedCreated.start <= currentDateRange.end
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
debug.log('Calendar createEvent response', {
|
debug.log('calendar', 'Calendar createEvent response', {
|
||||||
created: getStoreEventDebugSnapshot(created),
|
created: getStoreEventDebugSnapshot(created),
|
||||||
mappedCreated: getStoreEventDebugSnapshot(mappedCreated),
|
mappedCreated: getStoreEventDebugSnapshot(mappedCreated),
|
||||||
isVisible,
|
isVisible,
|
||||||
@@ -265,21 +265,21 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
debug.warn('Created event is hidden by current calendar filters', {
|
debug.warn('calendar', 'Created event is hidden by current calendar filters', {
|
||||||
selectedCalendarIds,
|
selectedCalendarIds,
|
||||||
createdCalendarIds,
|
createdCalendarIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inCurrentDateRange === false) {
|
if (inCurrentDateRange === false) {
|
||||||
debug.warn('Created event is outside the currently loaded date range', {
|
debug.warn('calendar', 'Created event is outside the currently loaded date range', {
|
||||||
currentDateRange,
|
currentDateRange,
|
||||||
createdStart: mappedCreated.start,
|
createdStart: mappedCreated.start,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedCreated.showWithoutTime && mappedCreated.timeZone !== null) {
|
if (mappedCreated.showWithoutTime && mappedCreated.timeZone !== null) {
|
||||||
debug.warn('Created all-day event came back with a non-null timeZone', {
|
debug.warn('calendar', 'Created all-day event came back with a non-null timeZone', {
|
||||||
timeZone: mappedCreated.timeZone,
|
timeZone: mappedCreated.timeZone,
|
||||||
event: getStoreEventDebugSnapshot(mappedCreated),
|
event: getStoreEventDebugSnapshot(mappedCreated),
|
||||||
});
|
});
|
||||||
@@ -301,7 +301,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
const storeEvent = get().events.find(e => e.id === id);
|
const storeEvent = get().events.find(e => e.id === id);
|
||||||
const realId = storeEvent?.originalId || id;
|
const realId = storeEvent?.originalId || id;
|
||||||
const targetAccountId = storeEvent?.accountId;
|
const targetAccountId = storeEvent?.accountId;
|
||||||
debug.log('Calendar updateEvent', {
|
debug.log('calendar', 'Calendar updateEvent', {
|
||||||
storeId: id,
|
storeId: id,
|
||||||
realId,
|
realId,
|
||||||
uid: storeEvent?.uid,
|
uid: storeEvent?.uid,
|
||||||
@@ -445,20 +445,20 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
await client.updateCalendarEvent(eventId, { calendarIds } as Partial<CalendarEvent>, undefined, targetAccountId);
|
await client.updateCalendarEvent(eventId, { calendarIds } as Partial<CalendarEvent>, undefined, targetAccountId);
|
||||||
linked++;
|
linked++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug.warn(`Import: failed to link event ${eventId} to target calendar:`, err);
|
debug.warn('calendar', `Import: failed to link event ${eventId} to target calendar:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linked > 0) {
|
if (linked > 0) {
|
||||||
debug.log(`Import: linked ${linked} existing events to target calendar`);
|
debug.log('calendar', `Import: linked ${linked} existing events to target calendar`);
|
||||||
}
|
}
|
||||||
const skipped = eventsToProcess.length - newEvents.length - eventsToLink.length;
|
const skipped = eventsToProcess.length - newEvents.length - eventsToLink.length;
|
||||||
if (skipped > 0) {
|
if (skipped > 0) {
|
||||||
debug.log(`Import: skipped ${skipped} events already in target calendar`);
|
debug.log('calendar', `Import: skipped ${skipped} events already in target calendar`);
|
||||||
}
|
}
|
||||||
eventsToProcess = newEvents;
|
eventsToProcess = newEvents;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.warn('Could not fetch existing events for deduplication, proceeding without:', error);
|
debug.warn('calendar', 'Could not fetch existing events for deduplication, proceeding without:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare all events for batch creation
|
// Prepare all events for batch creation
|
||||||
@@ -543,7 +543,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
const { created, failed } = await client.batchCreateCalendarEvents(batch, targetAccountId);
|
const { created, failed } = await client.batchCreateCalendarEvents(batch, targetAccountId);
|
||||||
imported += created.length;
|
imported += created.length;
|
||||||
if (failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
debug.warn(`Import batch ${i / BATCH_SIZE + 1}: ${failed.length} events failed`);
|
debug.warn('calendar', `Import batch ${i / BATCH_SIZE + 1}: ${failed.length} events failed`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error(`Import batch ${i / BATCH_SIZE + 1} failed:`, error);
|
debug.error(`Import batch ${i / BATCH_SIZE + 1} failed:`, error);
|
||||||
@@ -577,7 +577,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
debug.error('Failed to send cancellation emails:', e);
|
debug.error('Failed to send cancellation emails:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug.log('Calendar deleteEvent', {
|
debug.log('calendar', 'Calendar deleteEvent', {
|
||||||
storeId: id,
|
storeId: id,
|
||||||
realId,
|
realId,
|
||||||
uid: storeEvent?.uid,
|
uid: storeEvent?.uid,
|
||||||
@@ -675,7 +675,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
|
|
||||||
// If we couldn't destroy any events, stop to avoid infinite loop
|
// If we couldn't destroy any events, stop to avoid infinite loop
|
||||||
if (destroyed.length === 0) {
|
if (destroyed.length === 0) {
|
||||||
debug.warn('Could not delete any events, stopping clear loop. Not destroyed:', ids.length);
|
debug.warn('calendar', 'Could not delete any events, stopping clear loop. Not destroyed:', ids.length);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,7 +744,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
await get().refreshICalSubscription(client, subscription.id);
|
await get().refreshICalSubscription(client, subscription.id);
|
||||||
} catch {
|
} catch {
|
||||||
// Subscription created, initial fetch failed - user can retry
|
// Subscription created, initial fetch failed - user can retry
|
||||||
debug.warn('Initial subscription fetch failed for:', name);
|
debug.warn('calendar', 'Initial subscription fetch failed for:', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
@@ -892,7 +892,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
try {
|
try {
|
||||||
await get().refreshICalSubscription(client, sub.id);
|
await get().refreshICalSubscription(client, sub.id);
|
||||||
} catch {
|
} catch {
|
||||||
debug.warn('Failed to refresh subscription:', sub.name);
|
debug.warn('calendar', 'Failed to refresh subscription:', sub.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
|||||||
set({ sieveCapabilities: capabilities });
|
set({ sieveCapabilities: capabilities });
|
||||||
|
|
||||||
const allScripts = await client.getSieveScripts();
|
const allScripts = await client.getSieveScripts();
|
||||||
debug.log('Sieve scripts fetched:', allScripts.length);
|
debug.log('filters', 'Sieve scripts fetched:', allScripts.length);
|
||||||
|
|
||||||
// Skip the server-managed 'vacation' script (RFC 9661 §4) — it can only
|
// Skip the server-managed 'vacation' script (RFC 9661 §4) — it can only
|
||||||
// be modified via VacationResponse/set, not SieveScript/set.
|
// be modified via VacationResponse/set, not SieveScript/set.
|
||||||
@@ -73,10 +73,10 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
|||||||
const result = parseScript(content);
|
const result = parseScript(content);
|
||||||
|
|
||||||
if (result.isOpaque) {
|
if (result.isOpaque) {
|
||||||
debug.log('Sieve script is opaque (hand-edited)');
|
debug.log('filters', 'Sieve script is opaque (hand-edited)');
|
||||||
set({ isLoading: false, isOpaque: true, rules: [], vacationSettings: result.vacation || null });
|
set({ isLoading: false, isOpaque: true, rules: [], vacationSettings: result.vacation || null });
|
||||||
} else {
|
} else {
|
||||||
debug.log('Parsed', result.rules.length, 'filter rules');
|
debug.log('filters', 'Parsed', result.rules.length, 'filter rules');
|
||||||
set({ isLoading: false, isOpaque: false, rules: result.rules, vacationSettings: result.vacation || null });
|
set({ isLoading: false, isOpaque: false, rules: result.rules, vacationSettings: result.vacation || null });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -108,7 +108,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
set({ isSaving: false, rawScript: content });
|
set({ isSaving: false, rawScript: content });
|
||||||
debug.log('Filters saved successfully');
|
debug.log('filters', 'Filters saved successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to save filters:', error);
|
debug.error('Failed to save filters:', error);
|
||||||
set({
|
set({
|
||||||
@@ -212,7 +212,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.log('Vacation synced to sieve script');
|
debug.log('filters', 'Vacation synced to sieve script');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to sync vacation to sieve script:', error);
|
debug.error('Failed to sync vacation to sieve script:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
|
|||||||
{ id: 'spam', labelKey: 'spam' },
|
{ id: 'spam', labelKey: 'spam' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export type DebugCategory = 'jmap' | 'calendar' | 'tasks' | 'auth' | 'filters' | 'email' | 'push';
|
||||||
|
|
||||||
|
export const ALL_DEBUG_CATEGORIES: { id: DebugCategory; labelKey: string }[] = [
|
||||||
|
{ id: 'jmap', labelKey: 'jmap' },
|
||||||
|
{ id: 'calendar', labelKey: 'calendar' },
|
||||||
|
{ id: 'tasks', labelKey: 'tasks' },
|
||||||
|
{ id: 'auth', labelKey: 'auth' },
|
||||||
|
{ id: 'filters', labelKey: 'filters' },
|
||||||
|
{ id: 'email', labelKey: 'email' },
|
||||||
|
{ id: 'push', labelKey: 'push' },
|
||||||
|
];
|
||||||
|
|
||||||
export interface KeywordDefinition {
|
export interface KeywordDefinition {
|
||||||
id: string; // Used as JMAP keyword suffix: $label:<id>
|
id: string; // Used as JMAP keyword suffix: $label:<id>
|
||||||
label: string; // Display name
|
label: string; // Display name
|
||||||
@@ -175,6 +187,7 @@ interface SettingsState {
|
|||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
debugCategories: Record<DebugCategory, boolean>;
|
||||||
settingsSyncDisabled: boolean;
|
settingsSyncDisabled: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
@@ -299,6 +312,15 @@ const DEFAULT_SETTINGS = {
|
|||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
|
debugCategories: {
|
||||||
|
jmap: true,
|
||||||
|
calendar: true,
|
||||||
|
tasks: true,
|
||||||
|
auth: true,
|
||||||
|
filters: true,
|
||||||
|
email: true,
|
||||||
|
push: true,
|
||||||
|
} as Record<DebugCategory, boolean>,
|
||||||
settingsSyncDisabled: false,
|
settingsSyncDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -383,6 +405,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
sidebarApps: state.sidebarApps,
|
sidebarApps: state.sidebarApps,
|
||||||
keepAppsLoaded: state.keepAppsLoaded,
|
keepAppsLoaded: state.keepAppsLoaded,
|
||||||
debugMode: state.debugMode,
|
debugMode: state.debugMode,
|
||||||
|
debugCategories: state.debugCategories,
|
||||||
settingsSyncDisabled: state.settingsSyncDisabled,
|
settingsSyncDisabled: state.settingsSyncDisabled,
|
||||||
// Cross-store settings
|
// Cross-store settings
|
||||||
theme: useThemeStore.getState().theme,
|
theme: useThemeStore.getState().theme,
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
|
|||||||
setShowCompleted: (show) => set({ showCompleted: show }),
|
setShowCompleted: (show) => set({ showCompleted: show }),
|
||||||
|
|
||||||
fetchTasks: async (client, calendarIds) => {
|
fetchTasks: async (client, calendarIds) => {
|
||||||
debug.log('TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
|
debug.log('tasks', 'TaskStore/fetchTasks start', { calendarIds: calendarIds || 'all' });
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const tasks = await client.getCalendarTasks(calendarIds);
|
const tasks = await client.getCalendarTasks(calendarIds);
|
||||||
debug.log('TaskStore/fetchTasks received', tasks.length, 'tasks');
|
debug.log('tasks', 'TaskStore/fetchTasks received', tasks.length, 'tasks');
|
||||||
tasks.forEach((t, i) => {
|
tasks.forEach((t, i) => {
|
||||||
debug.log(`TaskStore/fetchTasks [${i}]`, {
|
debug.log('tasks', `TaskStore/fetchTasks [${i}]`, {
|
||||||
id: t.id, uid: t.uid, '@type': t['@type'],
|
id: t.id, uid: t.uid, '@type': t['@type'],
|
||||||
title: t.title, due: t.due, progress: t.progress,
|
title: t.title, due: t.due, progress: t.progress,
|
||||||
showWithoutTime: t.showWithoutTime, calendarIds: t.calendarIds,
|
showWithoutTime: t.showWithoutTime, calendarIds: t.calendarIds,
|
||||||
@@ -57,9 +57,9 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
createTask: async (client, task) => {
|
createTask: async (client, task) => {
|
||||||
debug.log('TaskStore/createTask', task);
|
debug.log('tasks', 'TaskStore/createTask', task);
|
||||||
const created = await client.createCalendarTask(task);
|
const created = await client.createCalendarTask(task);
|
||||||
debug.log('TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
|
debug.log('tasks', 'TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
|
||||||
set({ tasks: [...get().tasks, created] });
|
set({ tasks: [...get().tasks, created] });
|
||||||
return created;
|
return created;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user