diff --git a/server/index.ts b/server/index.ts index e5c624c..2b334c4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,7 +17,7 @@ import { PUBLIC_DIR } from '@/constants/paths'; import { cleanupDaemon, initSignal } from '@/modules/signal'; import admin from '@/routes/admin'; import ntfy from '@/routes/ntfy'; -import unifiedpush from '@/routes/unifiedpush'; +import unifiedpush from '@/routes/unified-push'; import { isLocalIP } from '@/utils/auth'; import { logError, logInfo, logVerbose, logWarn } from '@/utils/log'; @@ -33,7 +33,7 @@ if (!API_KEY) { if (PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD) { try { - const { startProtonMonitor } = await import('./modules/protonmail'); + const { startProtonMonitor } = await import('./modules/proton-mail'); await startProtonMonitor(); } catch (err) { logError('Failed to start Proton Mail monitor:', err); diff --git a/server/modules/protonmail.ts b/server/modules/proton-mail.ts similarity index 98% rename from server/modules/protonmail.ts rename to server/modules/proton-mail.ts index 0104494..eb3321c 100644 --- a/server/modules/protonmail.ts +++ b/server/modules/proton-mail.ts @@ -37,8 +37,6 @@ export async function startProtonMonitor() { password: PROTON_IMAP_PASSWORD, host: PROTON_BRIDGE_HOST, port: PROTON_BRIDGE_PORT, - tls: false, - tlsOptions: { rejectUnauthorized: false }, keepalive: true, }); @@ -87,9 +85,11 @@ export async function startProtonMonitor() { 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'; @@ -98,12 +98,15 @@ export async function startProtonMonitor() { 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; } } diff --git a/server/modules/signal.ts b/server/modules/signal.ts index 70bb510..2c21b5d 100644 --- a/server/modules/signal.ts +++ b/server/modules/signal.ts @@ -94,7 +94,7 @@ export async function finishLink() { return result; } -export async function unlinkDevice() { +async function unlinkDevice() { account = null; currentLinkUri = null; diff --git a/server/modules/store.ts b/server/modules/store.ts index 091308d..8bf5a10 100644 --- a/server/modules/store.ts +++ b/server/modules/store.ts @@ -1,6 +1,6 @@ import { Database } from 'bun:sqlite'; import { SUP_DB } from '@/constants/paths'; -import { createGroup } from './signal'; +import { createGroup } from '@/modules/signal'; interface EndpointMapping { endpoint: string; @@ -18,18 +18,18 @@ db.run(` ) `); -export const register = (endpoint: string, groupId: string, appName: string) => { +export const register = (endpoint: string, groupId: string, appName: string) => db.run('INSERT OR REPLACE INTO mappings (endpoint, groupId, appName) VALUES (?, ?, ?)', [ endpoint, groupId, appName, ]); -}; export const getGroupId = (endpoint: string) => { const row = db.query('SELECT groupId FROM mappings WHERE endpoint = ?').get(endpoint) as | { groupId: string } | undefined; + return row?.groupId; }; @@ -37,15 +37,15 @@ export const getAppName = (endpoint: string) => { const row = db.query('SELECT appName FROM mappings WHERE endpoint = ?').get(endpoint) as | { appName: string } | undefined; + return row?.appName; }; export const getAllMappings = () => db.query('SELECT endpoint, groupId, appName FROM mappings').all() as EndpointMapping[]; -export const remove = (endpoint: string) => { +export const remove = (endpoint: string) => db.run('DELETE FROM mappings WHERE endpoint = ?', [endpoint]); -}; export const getOrCreateGroup = async (key: string, name: string) => { const existingGroupId = getGroupId(key); @@ -53,5 +53,6 @@ export const getOrCreateGroup = async (key: string, name: string) => { const groupId = await createGroup(name); register(key, groupId, name); + return groupId; }; diff --git a/server/modules/unifiedpush.ts b/server/modules/unified-push.ts similarity index 100% rename from server/modules/unifiedpush.ts rename to server/modules/unified-push.ts diff --git a/server/routes/admin.ts b/server/routes/admin.ts index 66e5f6d..3a810f9 100644 --- a/server/routes/admin.ts +++ b/server/routes/admin.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import { basicAuth } from 'hono/basic-auth'; import { DEVICE_NAME, PROTON_IMAP_PASSWORD, PROTON_IMAP_USERNAME } from '@/constants/config'; -import { isImapConnected } from '@/modules/protonmail'; +import { isImapConnected } from '@/modules/proton-mail'; import { checkSignalCli, finishLink, @@ -19,133 +19,6 @@ let qrCacheTime = 0; let generatingPromise: Promise | null = null; const QR_CACHE_TTL = 10 * 60 * 1000; -export const handleHealthFragment = async () => { - const signalOk = await checkSignalCli(); - const linked = signalOk && (await hasValidAccount()); - const imap = isImapConnected(); - const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD; - const accountNumber = getAccount(); - - const html = ` -
-
- Signal Network: ${signalOk ? 'Connected' : 'Disconnected'} -
-
- Account: ${linked ? 'Linked' : 'Unlinked'} - ${linked && accountNumber ? `${formatPhoneNumber(accountNumber)}` : ''} -
- ${ - hasProtonConfig - ? `
- Proton Mail: ${imap ? 'Connected' : 'Disconnected'} - ${imap ? `${PROTON_IMAP_USERNAME}` : ''} -
` - : '' - } -
-
- ${await handleSignalInfoFragment()} -
- `; - - return { html, linked }; -}; - -export const handleSignalInfoFragment = async () => { - if (await hasValidAccount()) { - cachedQR = null; - return ``; - } - - return handleQRSection(); -}; - -export const handleEndpointsFragment = async () => { - const endpoints = getAllMappings(); - - if (endpoints.length === 0) { - return '

No endpoints registered

'; - } - - return ` - - `; -}; - -export const handleQRSection = async () => { - if (await hasValidAccount()) { - return '

Account already linked

'; - } - - const now = Date.now(); - - if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) { - generatingPromise = (async () => { - const qr = await generateLinkQR(); - cachedQR = qr; - qrCacheTime = Date.now(); - - finishLink() - .then(async () => { - await initSignal(); - }) - .catch(() => {}) - .finally(() => { - generatingPromise = null; - cachedQR = null; - qrCacheTime = 0; - }); - - return qr; - })(); - } - - if (generatingPromise && !cachedQR) { - await generatingPromise; - } - - return ` -

Scan this QR code with your Signal app:

-

Settings → Linked Devices → Link New Device

-
- QR Code -
- `; -}; - -export const handleLinkStatusCheck = async () => { - const linked = await hasValidAccount(); - return { linked }; -}; - const admin = new Hono(); admin.use( @@ -198,4 +71,126 @@ admin.delete('/action/delete-endpoint/:endpoint', async (c) => { return c.html(await handleEndpointsFragment()); }); +const handleHealthFragment = async () => { + const signalOk = await checkSignalCli(); + const linked = signalOk && (await hasValidAccount()); + const imap = isImapConnected(); + const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD; + const accountNumber = getAccount(); + + const html = ` +
+
+ Signal Network: ${signalOk ? 'Connected' : 'Disconnected'} +
+
+ Account: ${linked ? 'Linked' : 'Unlinked'} + ${linked && accountNumber ? `${formatPhoneNumber(accountNumber)}` : ''} +
+ ${ + hasProtonConfig + ? `
+ Proton Mail: ${imap ? 'Connected' : 'Disconnected'} + ${imap ? `${PROTON_IMAP_USERNAME}` : ''} +
` + : '' + } +
+
+ ${await handleSignalInfoFragment()} +
+ `; + + return { html, linked }; +}; + +const handleSignalInfoFragment = async () => { + if (await hasValidAccount()) { + cachedQR = null; + return ``; + } + + return handleQRSection(); +}; + +const handleEndpointsFragment = async () => { + const endpoints = getAllMappings(); + + if (endpoints.length === 0) { + return '

No endpoints registered

'; + } + + return ` + + `; +}; + +const handleQRSection = async () => { + if (await hasValidAccount()) { + return '

Account already linked

'; + } + + const now = Date.now(); + + if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) { + generatingPromise = (async () => { + const qr = await generateLinkQR(); + cachedQR = qr; + qrCacheTime = Date.now(); + + finishLink() + .then(async () => { + await initSignal(); + }) + .catch(() => {}) + .finally(() => { + generatingPromise = null; + cachedQR = null; + qrCacheTime = 0; + }); + + return qr; + })(); + } + + if (generatingPromise && !cachedQR) { + await generatingPromise; + } + + return ` +

Scan this QR code with your Signal app:

+

Settings → Linked Devices → Link New Device

+
+ QR Code +
+ `; +}; + export default admin; diff --git a/server/routes/unifiedpush.ts b/server/routes/unified-push.ts similarity index 98% rename from server/routes/unifiedpush.ts rename to server/routes/unified-push.ts index 0bc4a6e..07e866d 100644 --- a/server/routes/unifiedpush.ts +++ b/server/routes/unified-push.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import { sendGroupMessage } from '@/modules/signal'; import { getGroupId, getOrCreateGroup, remove } from '@/modules/store'; -import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unifiedpush'; +import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unified-push'; const unifiedpush = new Hono();