Aktueller Stand
This commit is contained in:
@@ -88,7 +88,7 @@ When you click **“Bereinigung starten / Start cleanup”** a cleanup job is cr
|
||||
|
||||
### The three checkboxes explained
|
||||
**Dry run (keine Änderungen)**
|
||||
Runs the full scan and logs what *would* happen, but **does not move/delete/unsubscribe** any mail. Useful for testing rules safely.
|
||||
Runs the full scan and logs what *would* happen, but **does not move/delete/unsubscribe** any mail and **does not send unsubscribe emails**. Useful for testing rules safely.
|
||||
|
||||
**Unsubscribe aktiv**
|
||||
Enables `List‑Unsubscribe` handling.
|
||||
|
||||
@@ -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(client);
|
||||
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);
|
||||
|
||||
const messages = await fetchHeaders(client, targetMailbox, 300, job.dryRun);
|
||||
messages = await fetchHeaders(imapClient, targetMailbox, 300, job.dryRun);
|
||||
} finally {
|
||||
await imapClient.logout().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let newsletterCount = 0;
|
||||
let processed = 0;
|
||||
|
||||
@@ -84,17 +137,21 @@ export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string)
|
||||
continue;
|
||||
}
|
||||
|
||||
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 client.mailboxCreate(action.target).catch(() => undefined);
|
||||
await client.messageMove(msg.uid, 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 client.messageDelete(msg.uid);
|
||||
await imapClient.messageDelete(msg.uid);
|
||||
await logJobEvent(cleanupJobId, "info", `Deleted message ${msg.uid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (job.unsubscribeEnabled) {
|
||||
const listUnsubscribe = msg.headers.get("list-unsubscribe") ?? null;
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -678,6 +678,9 @@ export default function App() {
|
||||
<input type="checkbox" checked={dryRun} onChange={(e) => setDryRun(e.target.checked)} />
|
||||
{t("cleanupDryRun")}
|
||||
</label>
|
||||
{dryRun && (
|
||||
<p className="hint-text">{t("cleanupDryRunHint")}</p>
|
||||
)}
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"cleanupDisabled": "Bereinigung ist noch nicht verfügbar.",
|
||||
"cleanupSelectMailbox": "Bitte ein Postfach auswählen.",
|
||||
"cleanupOauthRequired": "Bitte Gmail OAuth verbinden, bevor die Bereinigung startet.",
|
||||
"cleanupDryRunHint": "Dry run simuliert Routing und Unsubscribe. Es werden keine Änderungen durchgeführt und keine E-Mails gesendet.",
|
||||
"rulesTitle": "Regeln",
|
||||
"rulesName": "Rule Name",
|
||||
"rulesEnabled": "Rule aktiv",
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"cleanupDisabled": "Cleanup is not available yet.",
|
||||
"cleanupSelectMailbox": "Select a mailbox to start cleanup.",
|
||||
"cleanupOauthRequired": "Connect Gmail OAuth before starting cleanup.",
|
||||
"cleanupDryRunHint": "Dry run simulates routing and unsubscribe actions. No changes or emails are sent.",
|
||||
"rulesTitle": "Rules",
|
||||
"rulesName": "Rule name",
|
||||
"rulesEnabled": "Rule enabled",
|
||||
|
||||
Reference in New Issue
Block a user