Aktueller Stand
This commit is contained in:
@@ -1,5 +1,33 @@
|
||||
const headerIncludes = (headers: Map<string, string>, key: string) =>
|
||||
headers.has(key.toLowerCase());
|
||||
export type NewsletterConfig = {
|
||||
threshold: number;
|
||||
headerKeys: string[];
|
||||
subjectTokens: string[];
|
||||
fromTokens: string[];
|
||||
weightHeader: number;
|
||||
weightPrecedence: number;
|
||||
weightSubject: number;
|
||||
weightFrom: number;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: NewsletterConfig = {
|
||||
threshold: 2,
|
||||
headerKeys: [
|
||||
"list-unsubscribe",
|
||||
"list-id",
|
||||
"list-help",
|
||||
"list-archive",
|
||||
"list-post",
|
||||
"list-owner",
|
||||
"list-subscribe",
|
||||
"list-unsubscribe-post"
|
||||
],
|
||||
subjectTokens: ["newsletter", "unsubscribe", "update", "news", "digest"],
|
||||
fromTokens: ["newsletter", "no-reply", "noreply", "news", "updates"],
|
||||
weightHeader: 1,
|
||||
weightPrecedence: 1,
|
||||
weightSubject: 1,
|
||||
weightFrom: 1
|
||||
};
|
||||
|
||||
const headerValue = (headers: Map<string, string>, key: string) =>
|
||||
headers.get(key.toLowerCase()) ?? "";
|
||||
@@ -7,39 +35,64 @@ const headerValue = (headers: Map<string, string>, key: string) =>
|
||||
const containsAny = (value: string, tokens: string[]) =>
|
||||
tokens.some((token) => value.includes(token));
|
||||
|
||||
const normalizeList = (items: string[]) =>
|
||||
items.map((item) => item.trim().toLowerCase()).filter(Boolean);
|
||||
|
||||
export const detectNewsletter = (params: {
|
||||
headers: Map<string, string>;
|
||||
subject?: string | null;
|
||||
from?: string | null;
|
||||
config?: Partial<NewsletterConfig>;
|
||||
}) => {
|
||||
const subject = (params.subject ?? "").toLowerCase();
|
||||
const from = (params.from ?? "").toLowerCase();
|
||||
const headers = params.headers;
|
||||
const config: NewsletterConfig = {
|
||||
threshold: params.config?.threshold ?? DEFAULT_CONFIG.threshold,
|
||||
headerKeys: normalizeList(params.config?.headerKeys ?? DEFAULT_CONFIG.headerKeys),
|
||||
subjectTokens: normalizeList(params.config?.subjectTokens ?? DEFAULT_CONFIG.subjectTokens),
|
||||
fromTokens: normalizeList(params.config?.fromTokens ?? DEFAULT_CONFIG.fromTokens),
|
||||
weightHeader: params.config?.weightHeader ?? DEFAULT_CONFIG.weightHeader,
|
||||
weightPrecedence: params.config?.weightPrecedence ?? DEFAULT_CONFIG.weightPrecedence,
|
||||
weightSubject: params.config?.weightSubject ?? DEFAULT_CONFIG.weightSubject,
|
||||
weightFrom: params.config?.weightFrom ?? DEFAULT_CONFIG.weightFrom
|
||||
};
|
||||
|
||||
const hasListUnsubscribe = headerIncludes(headers, "list-unsubscribe");
|
||||
const hasListId = headerIncludes(headers, "list-id");
|
||||
|
||||
const matchedHeaderKeys = config.headerKeys.filter((key) => headers.has(key));
|
||||
const precedence = headerValue(headers, "precedence").toLowerCase();
|
||||
const bulkHeader = headerValue(headers, "x-precedence").toLowerCase();
|
||||
const precedenceHint = containsAny(precedence, ["bulk", "list"]) || containsAny(bulkHeader, ["bulk", "list"]);
|
||||
|
||||
const headerHints = containsAny(precedence, ["bulk", "list"]) ||
|
||||
containsAny(bulkHeader, ["bulk", "list"]) ||
|
||||
headerIncludes(headers, "list-unsubscribe-post");
|
||||
const subjectMatches = config.subjectTokens.filter((token) => subject.includes(token));
|
||||
const fromMatches = config.fromTokens.filter((token) => from.includes(token));
|
||||
|
||||
const subjectHints = containsAny(subject, ["newsletter", "unsubscribe", "update", "news", "digest"]);
|
||||
const fromHints = containsAny(from, ["newsletter", "no-reply", "noreply", "news", "updates"]);
|
||||
|
||||
const score = [hasListUnsubscribe, hasListId, headerHints, subjectHints, fromHints].filter(Boolean).length;
|
||||
const headerScore = matchedHeaderKeys.length * config.weightHeader;
|
||||
const precedenceScore = precedenceHint ? config.weightPrecedence : 0;
|
||||
const subjectScore = subjectMatches.length ? config.weightSubject : 0;
|
||||
const fromScore = fromMatches.length ? config.weightFrom : 0;
|
||||
const score = headerScore + precedenceScore + subjectScore + fromScore;
|
||||
|
||||
return {
|
||||
isNewsletter: score >= 2,
|
||||
isNewsletter: score >= config.threshold,
|
||||
score,
|
||||
signals: {
|
||||
hasListUnsubscribe,
|
||||
hasListId,
|
||||
headerHints,
|
||||
subjectHints,
|
||||
fromHints
|
||||
headerKeys: matchedHeaderKeys,
|
||||
precedenceHint,
|
||||
subjectTokens: subjectMatches,
|
||||
fromTokens: fromMatches,
|
||||
scoreBreakdown: {
|
||||
headerMatches: matchedHeaderKeys.length,
|
||||
headerWeight: config.weightHeader,
|
||||
headerScore,
|
||||
precedenceWeight: config.weightPrecedence,
|
||||
precedenceScore,
|
||||
subjectMatches: subjectMatches.length,
|
||||
subjectWeight: config.weightSubject,
|
||||
subjectScore,
|
||||
fromMatches: fromMatches.length,
|
||||
fromWeight: config.weightFrom,
|
||||
fromScore
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user