|
|
|
|
@@ -4,7 +4,7 @@ import { createImapClient, fetchHeaders, listMailboxes } from "./imap.js";
|
|
|
|
|
import { detectNewsletter } from "./newsletter.js";
|
|
|
|
|
import { matchRules } from "./rules.js";
|
|
|
|
|
import { unsubscribeFromHeader } from "./unsubscribe.js";
|
|
|
|
|
import { applyGmailAction } from "./gmail.js";
|
|
|
|
|
import { applyGmailAction, gmailClientForAccount } from "./gmail.js";
|
|
|
|
|
|
|
|
|
|
export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string) => {
|
|
|
|
|
const account = await prisma.mailboxAccount.findUnique({ where: { id: mailboxAccountId } });
|
|
|
|
|
@@ -31,17 +31,70 @@ export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string)
|
|
|
|
|
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Connecting to ${account.email}`);
|
|
|
|
|
|
|
|
|
|
const client = createImapClient(account);
|
|
|
|
|
await client.connect();
|
|
|
|
|
const isGmail = account.provider === "GMAIL";
|
|
|
|
|
const hasGmailOAuth = Boolean(account.oauthRefreshToken || account.oauthAccessToken);
|
|
|
|
|
|
|
|
|
|
let messages: {
|
|
|
|
|
uid: number;
|
|
|
|
|
subject?: string;
|
|
|
|
|
from?: string;
|
|
|
|
|
headers: Map<string, string>;
|
|
|
|
|
gmailMessageId?: string;
|
|
|
|
|
}[] = [];
|
|
|
|
|
|
|
|
|
|
let imapClient: ReturnType<typeof createImapClient> | null = null;
|
|
|
|
|
|
|
|
|
|
if (isGmail && hasGmailOAuth) {
|
|
|
|
|
const { gmail } = await gmailClientForAccount(account);
|
|
|
|
|
const list = await gmail.users.messages.list({
|
|
|
|
|
userId: "me",
|
|
|
|
|
labelIds: ["INBOX"],
|
|
|
|
|
maxResults: 300
|
|
|
|
|
});
|
|
|
|
|
const ids = list.data.messages?.map((msg) => msg.id).filter(Boolean) as string[] | undefined;
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Found ${ids?.length ?? 0} Gmail messages`, 10);
|
|
|
|
|
|
|
|
|
|
if (ids?.length) {
|
|
|
|
|
const headersWanted = ["Subject", "From", "List-Id", "List-Unsubscribe", "List-Unsubscribe-Post", "Message-Id"];
|
|
|
|
|
for (const id of ids) {
|
|
|
|
|
const meta = await gmail.users.messages.get({
|
|
|
|
|
userId: "me",
|
|
|
|
|
id,
|
|
|
|
|
format: "metadata",
|
|
|
|
|
metadataHeaders: headersWanted
|
|
|
|
|
});
|
|
|
|
|
const headers = new Map<string, string>();
|
|
|
|
|
const payloadHeaders = meta.data.payload?.headers ?? [];
|
|
|
|
|
for (const header of payloadHeaders) {
|
|
|
|
|
if (!header.name || !header.value) continue;
|
|
|
|
|
headers.set(header.name.toLowerCase(), header.value);
|
|
|
|
|
}
|
|
|
|
|
messages.push({
|
|
|
|
|
uid: 0,
|
|
|
|
|
subject: headers.get("subject"),
|
|
|
|
|
from: headers.get("from"),
|
|
|
|
|
headers,
|
|
|
|
|
gmailMessageId: id
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
imapClient = createImapClient(account);
|
|
|
|
|
await imapClient.connect();
|
|
|
|
|
try {
|
|
|
|
|
const mailboxes = await listMailboxes(imapClient);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Found ${mailboxes.length} mailboxes`, 10);
|
|
|
|
|
|
|
|
|
|
const targetMailbox = mailboxes.find((box) => /inbox/i.test(box.path))?.path ?? "INBOX";
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Scanning ${targetMailbox}`, 20);
|
|
|
|
|
|
|
|
|
|
messages = await fetchHeaders(imapClient, targetMailbox, 300, job.dryRun);
|
|
|
|
|
} finally {
|
|
|
|
|
await imapClient.logout().catch(() => undefined);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const mailboxes = await listMailboxes(client);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Found ${mailboxes.length} mailboxes`, 10);
|
|
|
|
|
|
|
|
|
|
const targetMailbox = mailboxes.find((box) => /inbox/i.test(box.path))?.path ?? "INBOX";
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Scanning ${targetMailbox}`, 20);
|
|
|
|
|
|
|
|
|
|
const messages = await fetchHeaders(client, targetMailbox, 300, job.dryRun);
|
|
|
|
|
let newsletterCount = 0;
|
|
|
|
|
let processed = 0;
|
|
|
|
|
|
|
|
|
|
@@ -84,14 +137,18 @@ export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string)
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((action.type === "MOVE" || action.type === "ARCHIVE" || action.type === "LABEL") && action.target) {
|
|
|
|
|
await client.mailboxCreate(action.target).catch(() => undefined);
|
|
|
|
|
await client.messageMove(msg.uid, action.target);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Moved message ${msg.uid} to ${action.target}`);
|
|
|
|
|
}
|
|
|
|
|
if (action.type === "DELETE") {
|
|
|
|
|
await client.messageDelete(msg.uid);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Deleted message ${msg.uid}`);
|
|
|
|
|
if (!imapClient) {
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", "Skipping IMAP action: no IMAP client");
|
|
|
|
|
} else {
|
|
|
|
|
if ((action.type === "MOVE" || action.type === "ARCHIVE" || action.type === "LABEL") && action.target) {
|
|
|
|
|
await imapClient.mailboxCreate(action.target).catch(() => undefined);
|
|
|
|
|
await imapClient.messageMove(msg.uid, action.target);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Moved message ${msg.uid} to ${action.target}`);
|
|
|
|
|
}
|
|
|
|
|
if (action.type === "DELETE") {
|
|
|
|
|
await imapClient.messageDelete(msg.uid);
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Deleted message ${msg.uid}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -155,10 +212,7 @@ export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await logJobEvent(cleanupJobId, "info", `Detected ${newsletterCount} newsletter candidates`, 80);
|
|
|
|
|
if (!job.dryRun) {
|
|
|
|
|
await client.mailboxClose();
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
await client.logout().catch(() => undefined);
|
|
|
|
|
// no-op
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|