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
|
### The three checkboxes explained
|
||||||
**Dry run (keine Änderungen)**
|
**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**
|
**Unsubscribe aktiv**
|
||||||
Enables `List‑Unsubscribe` handling.
|
Enables `List‑Unsubscribe` handling.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createImapClient, fetchHeaders, listMailboxes } from "./imap.js";
|
|||||||
import { detectNewsletter } from "./newsletter.js";
|
import { detectNewsletter } from "./newsletter.js";
|
||||||
import { matchRules } from "./rules.js";
|
import { matchRules } from "./rules.js";
|
||||||
import { unsubscribeFromHeader } from "./unsubscribe.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) => {
|
export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string) => {
|
||||||
const account = await prisma.mailboxAccount.findUnique({ where: { id: mailboxAccountId } });
|
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}`);
|
await logJobEvent(cleanupJobId, "info", `Connecting to ${account.email}`);
|
||||||
|
|
||||||
const client = createImapClient(account);
|
const isGmail = account.provider === "GMAIL";
|
||||||
await client.connect();
|
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 {
|
try {
|
||||||
const mailboxes = await listMailboxes(client);
|
const mailboxes = await listMailboxes(imapClient);
|
||||||
await logJobEvent(cleanupJobId, "info", `Found ${mailboxes.length} mailboxes`, 10);
|
await logJobEvent(cleanupJobId, "info", `Found ${mailboxes.length} mailboxes`, 10);
|
||||||
|
|
||||||
const targetMailbox = mailboxes.find((box) => /inbox/i.test(box.path))?.path ?? "INBOX";
|
const targetMailbox = mailboxes.find((box) => /inbox/i.test(box.path))?.path ?? "INBOX";
|
||||||
await logJobEvent(cleanupJobId, "info", `Scanning ${targetMailbox}`, 20);
|
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 newsletterCount = 0;
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
|
|
||||||
@@ -84,17 +137,21 @@ export const runCleanup = async (cleanupJobId: string, mailboxAccountId: string)
|
|||||||
continue;
|
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) {
|
if ((action.type === "MOVE" || action.type === "ARCHIVE" || action.type === "LABEL") && action.target) {
|
||||||
await client.mailboxCreate(action.target).catch(() => undefined);
|
await imapClient.mailboxCreate(action.target).catch(() => undefined);
|
||||||
await client.messageMove(msg.uid, action.target);
|
await imapClient.messageMove(msg.uid, action.target);
|
||||||
await logJobEvent(cleanupJobId, "info", `Moved message ${msg.uid} to ${action.target}`);
|
await logJobEvent(cleanupJobId, "info", `Moved message ${msg.uid} to ${action.target}`);
|
||||||
}
|
}
|
||||||
if (action.type === "DELETE") {
|
if (action.type === "DELETE") {
|
||||||
await client.messageDelete(msg.uid);
|
await imapClient.messageDelete(msg.uid);
|
||||||
await logJobEvent(cleanupJobId, "info", `Deleted message ${msg.uid}`);
|
await logJobEvent(cleanupJobId, "info", `Deleted message ${msg.uid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (job.unsubscribeEnabled) {
|
if (job.unsubscribeEnabled) {
|
||||||
const listUnsubscribe = msg.headers.get("list-unsubscribe") ?? null;
|
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);
|
await logJobEvent(cleanupJobId, "info", `Detected ${newsletterCount} newsletter candidates`, 80);
|
||||||
if (!job.dryRun) {
|
|
||||||
await client.mailboxClose();
|
|
||||||
}
|
|
||||||
} finally {
|
} 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)} />
|
<input type="checkbox" checked={dryRun} onChange={(e) => setDryRun(e.target.checked)} />
|
||||||
{t("cleanupDryRun")}
|
{t("cleanupDryRun")}
|
||||||
</label>
|
</label>
|
||||||
|
{dryRun && (
|
||||||
|
<p className="hint-text">{t("cleanupDryRunHint")}</p>
|
||||||
|
)}
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"cleanupDisabled": "Bereinigung ist noch nicht verfügbar.",
|
"cleanupDisabled": "Bereinigung ist noch nicht verfügbar.",
|
||||||
"cleanupSelectMailbox": "Bitte ein Postfach auswählen.",
|
"cleanupSelectMailbox": "Bitte ein Postfach auswählen.",
|
||||||
"cleanupOauthRequired": "Bitte Gmail OAuth verbinden, bevor die Bereinigung startet.",
|
"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",
|
"rulesTitle": "Regeln",
|
||||||
"rulesName": "Rule Name",
|
"rulesName": "Rule Name",
|
||||||
"rulesEnabled": "Rule aktiv",
|
"rulesEnabled": "Rule aktiv",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"cleanupDisabled": "Cleanup is not available yet.",
|
"cleanupDisabled": "Cleanup is not available yet.",
|
||||||
"cleanupSelectMailbox": "Select a mailbox to start cleanup.",
|
"cleanupSelectMailbox": "Select a mailbox to start cleanup.",
|
||||||
"cleanupOauthRequired": "Connect Gmail OAuth before starting cleanup.",
|
"cleanupOauthRequired": "Connect Gmail OAuth before starting cleanup.",
|
||||||
|
"cleanupDryRunHint": "Dry run simulates routing and unsubscribe actions. No changes or emails are sent.",
|
||||||
"rulesTitle": "Rules",
|
"rulesTitle": "Rules",
|
||||||
"rulesName": "Rule name",
|
"rulesName": "Rule name",
|
||||||
"rulesEnabled": "Rule enabled",
|
"rulesEnabled": "Rule enabled",
|
||||||
|
|||||||
Reference in New Issue
Block a user