new endpoint to mark emails as read

This commit is contained in:
lone-cloud 2026-01-27 10:20:59 -08:00
parent c3dce75eea
commit 13c068a8cf
6 changed files with 82 additions and 33 deletions

View file

@ -6,12 +6,12 @@
"name": "sup", "name": "sup",
"dependencies": { "dependencies": {
"chalk": "5.6.2", "chalk": "5.6.2",
"hono": "4.11.5", "hono": "4.11.7",
"hono-rate-limiter": "0.5.3", "hono-rate-limiter": "0.5.3",
"imap": "0.8.19", "imap": "0.8.19",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.12", "@biomejs/biome": "2.3.13",
"@types/bun": "1.3.6", "@types/bun": "1.3.6",
"@types/imap": "0.8.43", "@types/imap": "0.8.43",
"typescript": "5.9.3", "typescript": "5.9.3",
@ -19,23 +19,23 @@
}, },
}, },
"packages": { "packages": {
"@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
@ -49,7 +49,7 @@
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hono-rate-limiter": ["hono-rate-limiter@0.5.3", "", { "peerDependencies": { "hono": "^4.10.8", "unstorage": "^1.17.3" }, "optionalPeers": ["unstorage"] }, "sha512-M0DxbVMpPELEzLi0AJg1XyBHLGJXz7GySjsPoK+gc5YeeBsdGDGe+2RvVuCAv8ydINiwlbxqYMNxUEyYfRji/A=="], "hono-rate-limiter": ["hono-rate-limiter@0.5.3", "", { "peerDependencies": { "hono": "^4.10.8", "unstorage": "^1.17.3" }, "optionalPeers": ["unstorage"] }, "sha512-M0DxbVMpPELEzLi0AJg1XyBHLGJXz7GySjsPoK+gc5YeeBsdGDGe+2RvVuCAv8ydINiwlbxqYMNxUEyYfRji/A=="],

View file

@ -1,6 +1,6 @@
{ {
"name": "sup", "name": "sup",
"version": "0.1.3", "version": "0.1.4",
"description": "Privacy-preserving push notifications using Signal as transport", "description": "Privacy-preserving push notifications using Signal as transport",
"private": true, "private": true,
"type": "module", "type": "module",
@ -23,12 +23,12 @@
}, },
"dependencies": { "dependencies": {
"chalk": "5.6.2", "chalk": "5.6.2",
"hono": "4.11.5", "hono": "4.11.7",
"hono-rate-limiter": "0.5.3", "hono-rate-limiter": "0.5.3",
"imap": "0.8.19" "imap": "0.8.19"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.12", "@biomejs/biome": "2.3.13",
"@types/bun": "1.3.6", "@types/bun": "1.3.6",
"@types/imap": "0.8.43", "@types/imap": "0.8.43",
"typescript": "5.9.3" "typescript": "5.9.3"

View file

@ -27,28 +27,11 @@ try {
Pull the latest version: Pull the latest version:
\`\`\`bash \`\`\`bash
docker pull ghcr.io/lone-cloud/sup:${version} docker pull ghcr.io/lone-cloud/sup:${version}
\`\`\` \`\`\`" --generate-notes`;
Or use in your \`docker-compose.yml\`:
\`\`\`yaml
services:
server:
image: ghcr.io/lone-cloud/sup:${version}
\`\`\`
### Architectures
- linux/amd64
- linux/arm64
### Changes
See commit history for details." --generate-notes`;
console.log(` console.log(`
Release ${version} complete! Release ${version} complete!
GitHub release: https://github.com/lone-cloud/sup/releases/tag/${version}
GitHub Actions: https://github.com/lone-cloud/sup/actions
Once CI completes, images will be available: Once CI completes, images will be available:
docker pull ghcr.io/lone-cloud/sup:${version} docker pull ghcr.io/lone-cloud/sup:${version}
`); `);

View file

@ -16,6 +16,7 @@ import { PUBLIC_DIR } from '@/constants/paths';
import { cleanupDaemon, initSignal } from '@/modules/signal'; import { cleanupDaemon, initSignal } from '@/modules/signal';
import { admin } from '@/routes/admin'; import { admin } from '@/routes/admin';
import { ntfy } from '@/routes/ntfy'; import { ntfy } from '@/routes/ntfy';
import { protonMail } from '@/routes/proton-mail';
import { getLanIP, isLocalIP } from '@/utils/auth'; import { getLanIP, isLocalIP } from '@/utils/auth';
import { formatToCspString } from '@/utils/format'; import { formatToCspString } from '@/utils/format';
import { logError, logInfo, logVerbose, logWarn } from '@/utils/log'; import { logError, logInfo, logVerbose, logWarn } from '@/utils/log';
@ -80,6 +81,7 @@ app.use('*', serveStatic({ root: PUBLIC_DIR }));
app.route('/', ntfy); app.route('/', ntfy);
app.route('/', admin); app.route('/', admin);
app.route('/', protonMail);
app.notFound((c) => c.text('Not Found', 404)); app.notFound((c) => c.text('Not Found', 404));

View file

@ -14,6 +14,7 @@ let imapConnected = false;
let monitorStartTime = 0; let monitorStartTime = 0;
let reconnectAttempts = 0; let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 300000; // 5 minutes const MAX_RECONNECT_DELAY = 300000; // 5 minutes
let imapInstance: Imap | null = null;
export const isImapConnected = () => imapConnected; export const isImapConnected = () => imapConnected;
@ -167,4 +168,28 @@ export async function startProtonMonitor() {
process.on('SIGTERM', () => imap.end()); process.on('SIGTERM', () => imap.end());
process.on('SIGINT', () => imap.end()); process.on('SIGINT', () => imap.end());
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, '\\Seen', (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 });
}
});
} }

View file

@ -0,0 +1,39 @@
import { Hono } from 'hono';
import { basicAuth } from 'hono/basic-auth';
import { markEmailAsRead } from '@/modules/proton-mail';
import { verifyApiKey } from '@/utils/auth';
import { logError, logVerbose } from '@/utils/log';
export const protonMail = new Hono();
protonMail.use(
'*',
basicAuth({
verifyUser: (_, password, c) => verifyApiKey(password, c),
realm: 'SUP Proton Mail - Username: any, Password: API_KEY',
}),
);
protonMail.post('/api/proton-mail/mark-read', async (c) => {
try {
const body = await c.req.json();
const { uid } = body;
if (!uid || typeof uid !== 'number') {
return c.json({ error: 'uid (number) is required' }, 400);
}
const result = await markEmailAsRead(uid);
if (!result.success) {
return c.json({ error: result.error }, 500);
}
logVerbose(`Marked email as read: UID ${uid}`);
return c.json({ success: true });
} catch (error) {
logError('Failed to mark email as read:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});