Add manual Scan Inbox button for email monitoring
- Add scanInbox function that opens a separate IMAP connection and processes all unseen messages in the configured folder - Wire to email-watch API as action "scan" - Add Scan Inbox button to settings page next to Start/Stop Monitoring - Useful for processing emails that arrived before monitoring started, since IMAP IDLE only notifies about new messages Made-with: Cursor
This commit is contained in:
parent
b2c29120cd
commit
aacf9c776b
3 changed files with 112 additions and 2 deletions
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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<MondayColumn[]>([]);
|
||||
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"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-xl"
|
||||
onClick={scanInbox}
|
||||
disabled={scanning || !settings.emailImapHost || !settings.emailImapUser || !settings.emailImapPass}
|
||||
>
|
||||
{scanning ? <Loader2 className="mr-2 size-3 animate-spin" /> : <RefreshCw className="mr-2 size-3" />}
|
||||
Scan Inbox
|
||||
</Button>
|
||||
{settings.emailWatching && (
|
||||
<Badge className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-300">
|
||||
Active
|
||||
|
|
|
|||
|
|
@ -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<EmailConfig, "processedAction" | "processedFolder">): Promise<{ ok: boolean; folders?: string[]; error?: string }> {
|
||||
let testClient: ImapFlow | null = null;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue