diff --git a/README.md b/README.md index 08e5a2e..6985172 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ docker compose up -d ### 3. ProtonMail Integration (Optional) +> **Note:** The default ProtonMail Bridge image uses `shenxn/protonmail-bridge:build` which compiles from source and supports multiple architectures. For x86_64 systems, you can use `shenxn/protonmail-bridge:latest` (pre-built binary, smaller and faster). For ARM devices (Raspberry Pi), stick with `:build`. + To receive ProtonMail notifications via Signal: 1. **Initialize ProtonMail Bridge** (one-time setup): diff --git a/server/index.ts b/server/index.ts index a3026b8..3367eeb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import { API_KEY, BRIDGE_IMAP_PASSWORD, BRIDGE_IMAP_USERNAME, PORT } from './constants/config'; import { ROUTES } from './constants/server'; import { checkSignalCli, initSignal, startDaemon } from './modules/signal'; @@ -13,6 +12,7 @@ import { handleUnregister, } from './routes/unifiedpush'; import { withAuth, withFormAuth } from './utils/auth'; +import { logError, logInfo, logSuccess, logWarn } from './utils/log'; let daemon: ReturnType | null = null; @@ -22,24 +22,28 @@ try { const hasAccount = isLinked && (await initSignal({})); if (hasAccount) { - console.log(chalk.green('✓ Signal account linked')); + logSuccess('✓ Signal account linked'); } else { - console.log(chalk.yellow('⚠ No Signal account linked')); - console.log(chalk.dim(` Visit http://localhost:${PORT}/link to link your device`)); + logWarn('⚠ No Signal account linked'); + logInfo(` Visit http://localhost:${PORT}/link to link your device`); } } catch (error) { - console.error(chalk.red('✗ Failed to start signal-cli daemon')); - console.error(chalk.dim(` ${error instanceof Error ? error.message : String(error)}`)); + logError(` ${error instanceof Error ? error.message : String(error)}`); } if (!API_KEY) { - console.warn(chalk.yellow('⚠️ Server running without API_KEY')); - console.warn(chalk.dim(' Set API_KEY env var for production deployments.')); + logWarn('⚠️ Server running without API_KEY'); + console.warn(' Set API_KEY env var for production deployments.'); } if (BRIDGE_IMAP_USERNAME && BRIDGE_IMAP_PASSWORD) { - const { startProtonMonitor } = await import('./modules/protonmail'); - await startProtonMonitor(); + try { + const { startProtonMonitor } = await import('./modules/protonmail'); + await startProtonMonitor(); + } catch (err) { + logError('❌ Failed to start ProtonMail monitor:', err); + logWarn('⚠️ Continuing without ProtonMail integration'); + } } const server = Bun.serve({ @@ -99,4 +103,4 @@ const server = Bun.serve({ }, }); -console.log(chalk.cyan.bold(`\n🚀 SUP running on http://localhost:${server.port}`)); +logInfo(`\n🚀 SUP running on http://localhost:${server.port}`); diff --git a/server/modules/protonmail.ts b/server/modules/protonmail.ts index c1346e0..6c25bde 100644 --- a/server/modules/protonmail.ts +++ b/server/modules/protonmail.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import Imap from 'imap'; import { BRIDGE_IMAP_PASSWORD, @@ -9,32 +8,37 @@ import { PROTON_BRIDGE_PORT, SUP_TOPIC, } from '../constants/config'; -import { log } from '../utils/log'; +import { logError, logInfo, logSuccess, logVerbose, logWarn } from '../utils/log'; import { createGroup, sendGroupMessage } from './signal'; import { getGroupId, register } from './store'; export async function startProtonMonitor() { if (!BRIDGE_IMAP_USERNAME || !BRIDGE_IMAP_PASSWORD) { - console.error( - chalk.red('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD'), - ); - console.error(chalk.yellow('Run: docker compose run --rm protonmail-bridge init')); - console.error(chalk.yellow('Then use `login` and `info` commands to get IMAP credentials')); + 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; } - log(chalk.blue(`🔗 Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`)); - log(chalk.blue(`📨 Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`)); + 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: true, - tlsOptions: { rejectUnauthorized: false }, - keepalive: true, - }); + let imap: Imap; + try { + imap = new Imap({ + user: BRIDGE_IMAP_USERNAME, + password: BRIDGE_IMAP_PASSWORD, + host: PROTON_BRIDGE_HOST, + port: PROTON_BRIDGE_PORT, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + keepalive: true, + }); + } catch (err) { + logError('❌ Failed to initialize IMAP client:', err); + logWarn('⚠️ ProtonMail integration disabled (bridge not reachable)'); + return; + } async function sendNotification(title: string, message: string) { try { @@ -50,23 +54,23 @@ export async function startProtonMonitor() { : ''; await sendGroupMessage(groupId, `${prefix}**${title}**\n${message}`); - console.log(chalk.green(`✅ Notification sent: ${title}`)); + logSuccess(`✅ Notification sent: ${title}`); } catch (error) { - console.error(chalk.red('❌ Failed to send notification:'), error); + logError('❌ Failed to send notification:', error); } } function openInbox() { imap.openBox('INBOX', false, (err, box) => { if (err) { - console.error(chalk.red('Failed to open inbox:'), err); + logError('Failed to open inbox:', err); return; } - log(`✅ Connected to inbox (${box.messages.total} messages)`); + logVerbose(`✅ Connected to inbox (${box.messages.total} messages)`); imap.on('mail', async (numNewMsgs: number) => { - log(`📬 ${numNewMsgs} new message(s) received`); + logVerbose(`📬 ${numNewMsgs} new message(s) received`); const fetch = imap.seq.fetch(`${box.messages.total}:*`, { bodies: 'HEADER.FIELDS (FROM SUBJECT)', @@ -91,26 +95,38 @@ export async function startProtonMonitor() { }); imap.on('update', () => { - log('📊 Mailbox updated'); + logVerbose('📊 Mailbox updated'); }); }); } imap.once('ready', () => { - log('✅ IMAP connection ready'); + logVerbose('✅ IMAP connection ready'); openInbox(); }); imap.once('error', (err: Error) => { - console.error(chalk.red('❌ IMAP error:'), err); + logError('❌ IMAP error:', err); + logWarn('⚠️ ProtonMail integration disabled due to connection error'); }); imap.once('end', () => { - log('⚠️ IMAP connection ended, reconnecting...'); - setTimeout(() => imap.connect(), 5000); + logVerbose('⚠️ IMAP connection ended, reconnecting...'); + setTimeout(() => { + try { + imap.connect(); + } catch (err) { + logError('❌ Failed to reconnect:', err); + } + }, 5000); }); - imap.connect(); + try { + imap.connect(); + } catch (err) { + logError('❌ Failed to connect to Proton Bridge:', err); + logWarn('⚠️ ProtonMail integration disabled'); + } process.on('SIGTERM', () => { imap.end(); diff --git a/server/modules/signal.ts b/server/modules/signal.ts index 6e3a7fd..0a9bddc 100644 --- a/server/modules/signal.ts +++ b/server/modules/signal.ts @@ -1,12 +1,11 @@ import { rm } from 'node:fs/promises'; -import chalk from 'chalk'; import { DAEMON_START_MAX_ATTEMPTS, DEVICE_NAME, VERBOSE } from '../constants/config'; import { SIGNAL_CLI, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '../constants/paths'; import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '../types'; -import { log } from '../utils/log'; +import { logError, logInfo, logSuccess, logVerbose, logWarn } from '../utils/log'; import { call } from '../utils/rpc'; -log(`Running signal-cli from ${SIGNAL_CLI}`); +logVerbose(`Running signal-cli from ${SIGNAL_CLI}`); let account: string | null = null; let currentLinkUri: string | null = null; @@ -141,19 +140,19 @@ export async function startDaemon() { if (VERBOSE) { if (trimmed.includes('ERROR')) { - console.error(chalk.red('[signal-cli]'), trimmed); + logError('[signal-cli]', trimmed); } else if (trimmed.includes('WARN')) { - console.warn(chalk.yellow('[signal-cli]'), trimmed); + logWarn('[signal-cli]', trimmed); } else { - console.log(chalk.dim('[signal-cli]'), trimmed); + logInfo('[signal-cli]', trimmed); } continue; } if (trimmed.includes('WARN')) { - console.warn(chalk.yellow('[signal-cli]'), trimmed); + logWarn('[signal-cli]', trimmed); } else if (trimmed.includes('ERROR') || !trimmed.includes('INFO')) { - console.error(chalk.red('[signal-cli]'), trimmed); + logError('[signal-cli]', trimmed); } } })(); @@ -169,11 +168,11 @@ export async function startDaemon() { }, }); socket.end(); - console.log(chalk.green('✓ signal-cli daemon started')); + logSuccess('✓ signal-cli daemon started'); return proc; } catch (_error) { if (authError && attempts > 5 && !cleaned) { - console.log(chalk.yellow('⚠ Detected stale account data, cleaning up and retrying...')); + logWarn('⚠ Detected stale account data, cleaning up and retrying...'); proc.kill(); await unlinkDevice(); cleaned = true; diff --git a/server/utils/log.ts b/server/utils/log.ts index 898ddd0..f96854c 100644 --- a/server/utils/log.ts +++ b/server/utils/log.ts @@ -1,3 +1,9 @@ +import chalk from 'chalk'; import { VERBOSE } from '../constants/config'; -export const log = (...args: unknown[]) => VERBOSE && console.log(...args); +export const logVerbose = (...args: unknown[]) => VERBOSE && console.log(...args); + +export const logError = (...args: unknown[]) => console.error(chalk.red(...args)); +export const logWarn = (...args: unknown[]) => console.warn(chalk.yellow(...args)); +export const logInfo = (...args: unknown[]) => console.log(chalk.blue(...args)); +export const logSuccess = (...args: unknown[]) => console.log(chalk.green(...args));