import Imap from 'imap'; import { ACTION_MARK_READ, ENDPOINT_PREFIX_PROTON, IMAP_INBOX, IMAP_MAX_RECONNECT_DELAY, IMAP_RECONNECT_BASE_DELAY, IMAP_SEEN_FLAG, PROTON_BRIDGE_HOST, PROTON_BRIDGE_PORT, PROTON_IMAP_PASSWORD, PROTON_IMAP_USERNAME, PROTON_PRISM_TOPIC, } from '@/constants/config'; import { sendNotification as sendChannelNotification } from '@/modules/notifications'; import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log'; let imapConnected = false; let monitorStartTime = 0; let reconnectAttempts = 0; let imapInstance: Imap | null = null; let reconnectTimeout: Timer | null = null; export const isImapConnected = () => imapConnected; const scheduleReconnect = () => { const delay = Math.min( IMAP_RECONNECT_BASE_DELAY * 2 ** reconnectAttempts, IMAP_MAX_RECONNECT_DELAY, ); reconnectAttempts++; logInfo(`Reconnecting in ${delay / 1000}s (attempt ${reconnectAttempts})...`); reconnectTimeout = setTimeout(() => { if (!imapConnected && imapInstance) { logInfo('Reconnecting to Proton Bridge...'); imapInstance.connect(); } }, delay); }; export async function startProtonMonitor() { if (!PROTON_IMAP_USERNAME || !PROTON_IMAP_PASSWORD) { logError('Missing required env vars: PROTON_IMAP_USERNAME and PROTON_IMAP_PASSWORD'); logWarn('Run: docker compose run --rm protonmail-bridge init'); logWarn('Then use `login` and `info` commands to get IMAP credentials'); return; } logInfo(`Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`); logInfo(`Monitoring mailbox: ${PROTON_IMAP_USERNAME}`); const imap = new Imap({ user: PROTON_IMAP_USERNAME, password: PROTON_IMAP_PASSWORD, host: PROTON_BRIDGE_HOST, port: PROTON_BRIDGE_PORT, keepalive: true, }); const openInbox = () => imap.openBox(IMAP_INBOX, false, (err, box) => { if (err) { logError('Failed to open inbox:', err); return; } if (!monitorStartTime) { monitorStartTime = Date.now(); } else { logVerbose('Inbox reopened (reconnection)'); } imap.on('mail', async (numNewMsgs: number) => { logVerbose(`${numNewMsgs} new message(s) in mailbox`); const fetch = imap.seq.fetch(`${box.messages.total - numNewMsgs + 1}:*`, { bodies: 'HEADER.FIELDS (FROM SUBJECT DATE)', struct: true, }); fetch.on('message', async (msg) => { const uidPromise = new Promise((resolve) => msg.once('attributes', (attrs) => { resolve(attrs.uid); }), ); msg.on('body', (stream) => { let buffer = ''; stream.on('data', (chunk) => { buffer += chunk.toString('utf8'); }); stream.once('end', async () => { const header = Imap.parseHeader(buffer); const rawFrom = header.from?.[0] || 'Unknown sender'; const subject = header.subject?.[0] || 'No subject'; const dateStr = header.date?.[0]; if (dateStr) { const messageDate = new Date(dateStr).getTime(); if (messageDate < monitorStartTime) { const formattedDate = new Date(dateStr).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short', }); logVerbose(`Skipping old email: ${subject} (${formattedDate})`); return; } } const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s* { imapConnected = true; reconnectAttempts = 0; logSuccess('IMAP is ready'); openInbox(); }); imap.on('error', (err: Error) => { imapConnected = false; logError('IMAP error:', err.message); }); imap.on('close', (hadError: boolean) => { imapConnected = false; logError(`IMAP connection closed (hadError: ${hadError})`); scheduleReconnect(); }); imap.on('end', () => { imapConnected = false; logError('IMAP connection ended by server'); scheduleReconnect(); }); imap.connect(); const cleanup = () => { if (reconnectTimeout) clearTimeout(reconnectTimeout); imap.end(); }; process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); imapInstance = imap; } export async function markEmailAsRead(uid: number) { if (!imapInstance || !imapConnected) { return { success: false, error: 'IMAP not connected' }; } return new Promise<{ success: boolean; error?: string }>((resolve) => { try { imapInstance?.addFlags(uid, IMAP_SEEN_FLAG, (err) => { if (err) { logError('Failed to mark email as read:', err); resolve({ success: false, error: err.message }); } else { resolve({ success: true }); } }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; resolve({ success: false, error: errorMessage }); } }); }