diff --git a/src/app/api/email-watch/route.ts b/src/app/api/email-watch/route.ts index 2080c7d..dc3f9e6 100644 --- a/src/app/api/email-watch/route.ts +++ b/src/app/api/email-watch/route.ts @@ -4,6 +4,7 @@ import { startEmailWatching, stopEmailWatching, isEmailWatching, + scanInbox, } from "@/lib/email-watcher"; export async function POST(request: NextRequest) { @@ -11,13 +12,21 @@ export async function POST(request: NextRequest) { const body = await request.json().catch(() => ({})); const action = body.action as string | undefined; - if (!action || !["start", "stop"].includes(action)) { + if (!action || !["start", "stop", "scan"].includes(action)) { return NextResponse.json( - { error: "Invalid action. Use 'start' or 'stop'" }, + { error: "Invalid action. Use 'start', 'stop', or 'scan'" }, { status: 400 } ); } + if (action === "scan") { + const result = await scanInbox(); + if (result.error) { + return NextResponse.json({ error: result.error, ...result }, { status: 500 }); + } + return NextResponse.json({ ok: true, ...result }); + } + if (action === "start") { const settings = await prisma.appSettings.findUnique({ where: { id: "singleton" }, diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 1ddfd2f..9dde639 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -189,6 +189,7 @@ export default function SettingsPage() { const [cleanupStatus, setCleanupStatus] = React.useState<{ sourcesEligible: number; imagesEligible: number } | null>(null); const [cleaning, setCleaning] = React.useState(false); const [emailTestStatus, setEmailTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle"); + const [scanning, setScanning] = React.useState(false); const [mondayColumns, setMondayColumns] = React.useState([]); const [fetchingColumns, setFetchingColumns] = React.useState(false); const [subscribing, setSubscribing] = React.useState(false); @@ -327,6 +328,32 @@ export default function SettingsPage() { } }; + const scanInbox = async () => { + setScanning(true); + try { + const res = await fetch("/api/email-watch", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "scan" }), + }); + const data = await res.json(); + if (!res.ok) { + toast.error(data.error || "Scan failed"); + } else if (data.processed === 0 && data.skipped === 0) { + toast.info("No unread emails found in inbox"); + } else { + const parts: string[] = []; + if (data.processed > 0) parts.push(`${data.processed} processed`); + if (data.skipped > 0) parts.push(`${data.skipped} skipped`); + toast.success(`Inbox scan: ${parts.join(", ")}`); + } + } catch { + toast.error("Failed to scan inbox"); + } finally { + setScanning(false); + } + }; + const testEmailConnection = async () => { setEmailTestStatus("testing"); try { @@ -925,6 +952,16 @@ export default function SettingsPage() { > {settings.emailWatching ? "Stop Monitoring" : "Start Monitoring"} + {settings.emailWatching && ( Active diff --git a/src/lib/email-watcher.ts b/src/lib/email-watcher.ts index 0a6910c..694ab74 100644 --- a/src/lib/email-watcher.ts +++ b/src/lib/email-watcher.ts @@ -241,6 +241,70 @@ export function getEmailWatchStatus() { }; } +export async function scanInbox(): Promise<{ processed: number; skipped: number; error?: string }> { + const settings = await prisma.appSettings.findUnique({ where: { id: "singleton" } }); + if (!settings?.emailImapHost || !settings?.emailImapUser || !settings?.emailImapPass) { + return { processed: 0, skipped: 0, error: "IMAP not configured" }; + } + + const config: EmailConfig = { + host: settings.emailImapHost, + port: settings.emailImapPort, + user: settings.emailImapUser, + pass: settings.emailImapPass, + tls: settings.emailImapTls, + folder: settings.emailFolder, + processedAction: settings.emailProcessed, + processedFolder: settings.emailProcessedFolder, + }; + + const scanClient = new ImapFlow({ + host: config.host, + port: config.port, + secure: config.tls, + auth: { user: config.user, pass: config.pass }, + logger: false, + emitLogs: false, + }); + + let processed = 0; + let skipped = 0; + + try { + await scanClient.connect(); + log(`[scan] Connected for manual inbox scan`); + + const lock = await scanClient.getMailboxLock(config.folder); + try { + const searchResult = await scanClient.search({ seen: false }, { uid: true }); + const unseen = Array.isArray(searchResult) ? searchResult : []; + log(`[scan] Found ${unseen.length} unseen message(s)`); + + for (const uid of unseen) { + try { + await handleMessage(scanClient, uid, config); + processed++; + } catch (err) { + logError(`[scan] Error handling message UID ${uid}:`, err); + skipped++; + } + } + } finally { + lock.release(); + } + + await scanClient.logout(); + } catch (err) { + const message = err instanceof Error ? err.message : "Scan failed"; + logError("[scan] Error:", message); + try { await scanClient.logout(); } catch {} + return { processed, skipped, error: message }; + } + + log(`[scan] Complete: ${processed} processed, ${skipped} skipped`); + return { processed, skipped }; +} + export async function testEmailConnection(config: Omit): Promise<{ ok: boolean; folders?: string[]; error?: string }> { let testClient: ImapFlow | null = null; try {