import Imap from 'imap'; import { BRIDGE_IMAP_PASSWORD, BRIDGE_IMAP_USERNAME, ENABLE_PROTON_ANDROID, LAUNCH_ENDPOINT_PREFIX, PROTON_BRIDGE_HOST, PROTON_BRIDGE_PORT, SUP_TOPIC, } from '@/constants/config'; import { createGroup, hasValidAccount, sendGroupMessage } from '@/modules/signal'; import { getGroupId, register } from '@/modules/store'; import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log'; let imapConnected = false; export const isImapConnected = () => imapConnected; export async function startProtonMonitor() { if (!BRIDGE_IMAP_USERNAME || !BRIDGE_IMAP_PASSWORD) { logError('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD'); logWarn('Run: docker compose run --rm protonmail-bridge init'); logWarn('Then use `login` and `info` commands to get IMAP credentials'); return; } const linked = await hasValidAccount(); if (!linked) { logWarn('Signal account not linked. ProtonMail notifications will be skipped.'); logWarn('Link your Signal account at /link to enable email notifications.'); } logInfo(`Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`); logInfo(`Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`); const imap = new Imap({ user: BRIDGE_IMAP_USERNAME, password: BRIDGE_IMAP_PASSWORD, host: PROTON_BRIDGE_HOST, port: PROTON_BRIDGE_PORT, tls: false, tlsOptions: { rejectUnauthorized: false }, keepalive: true, }); async function sendNotification(title: string, message: string) { if (!(await hasValidAccount())) { logVerbose('Skipping notification (Signal not linked)'); return; } try { const topicKey = `proton-${SUP_TOPIC}`; const groupId = getGroupId(topicKey) ?? (await createGroup(SUP_TOPIC)); if (!getGroupId(topicKey)) { register(topicKey, groupId, SUP_TOPIC); } if (ENABLE_PROTON_ANDROID) { await sendGroupMessage( groupId, `${LAUNCH_ENDPOINT_PREFIX}ch.protonmail.android]\n**${title}**\n${message}`, ); } else { await sendGroupMessage(groupId, `${title}\n${message}`); } logVerbose(`Notification sent: ${title}`); } catch (error) { logError('Failed to send notification:', error); } } function openInbox() { imap.openBox('INBOX', false, (err, box) => { if (err) { logError('Failed to open inbox:', err); return; } imap.on('mail', async (numNewMsgs: number) => { logVerbose(`${numNewMsgs} new message(s) received`); const fetch = imap.seq.fetch(`${box.messages.total}:*`, { bodies: 'HEADER.FIELDS (FROM SUBJECT)', struct: true, }); fetch.on('message', (msg) => { msg.on('body', (stream) => { let buffer = ''; stream.on('data', (chunk) => { buffer += chunk.toString('utf8'); }); stream.once('end', () => { const header = Imap.parseHeader(buffer); const rawFrom = header.from?.[0] || 'Unknown sender'; const subject = header.subject?.[0] || 'No subject'; const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s* { imapConnected = true; 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})`); }); imap.on('end', () => { imapConnected = false; logError('IMAP connection ended by server'); }); imap.connect(); process.on('SIGTERM', () => { imap.end(); }); process.on('SIGINT', () => { imap.end(); }); }