mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-07-26 16:28:01 +08:00
fix: 按规范处理 SPF、DKIM 和 DMARC 认证结果 (#1085)
fix: align SPF, DKIM, and DMARC junk mail checks with RFC standards
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getBooleanValue, getStringArray } from "../utils";
|
||||
import { commonParseMail } from "../common";
|
||||
import { isJunkMailByHeaders } from "./junk_mail_policy";
|
||||
|
||||
export const check_if_junk_mail = async (
|
||||
env: Bindings, address: string,
|
||||
@@ -14,50 +15,9 @@ export const check_if_junk_mail = async (
|
||||
|
||||
const checkListWhenExist = getStringArray(env.JUNK_MAIL_CHECK_LIST);
|
||||
const forcePassList = getStringArray(env.JUNK_MAIL_FORCE_PASS_LIST);
|
||||
const passedList: string[] = [];
|
||||
const existList: string[] = [];
|
||||
|
||||
const headers = parsedEmail.headers;
|
||||
for (const header of headers) {
|
||||
if (!header["key"]) continue;
|
||||
if (!header["value"]) continue;
|
||||
|
||||
// check spf
|
||||
if (header["key"].toLowerCase() == "received-spf") {
|
||||
existList.push("spf");
|
||||
if (header["value"].toLowerCase().includes("pass")) {
|
||||
passedList.push("spf");
|
||||
}
|
||||
}
|
||||
|
||||
// check dkim and dmarc
|
||||
if (header["key"].toLowerCase() == "authentication-results") {
|
||||
if (header["value"].toLowerCase().includes("dkim=")) {
|
||||
existList.push("dkim");
|
||||
if (header["value"].toLowerCase().includes("dkim=pass")) {
|
||||
passedList.push("dkim");
|
||||
}
|
||||
}
|
||||
if (header["value"].toLowerCase().includes("dmarc=")) {
|
||||
existList.push("dmarc");
|
||||
if (header["value"].toLowerCase().includes("dmarc=pass")) {
|
||||
passedList.push("dmarc");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if all checkListWhenExist item passed when exist
|
||||
if (checkListWhenExist?.some(
|
||||
(checkName) => existList.includes(checkName.toLowerCase())
|
||||
&& !passedList.includes(checkName.toLowerCase())
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (forcePassList?.length == 0) return false;
|
||||
|
||||
// check force pass list
|
||||
return forcePassList.some(
|
||||
(checkName) => !passedList.includes(checkName.toLowerCase())
|
||||
return isJunkMailByHeaders(
|
||||
parsedEmail.headers,
|
||||
checkListWhenExist,
|
||||
forcePassList,
|
||||
);
|
||||
}
|
||||
|
||||
88
worker/src/email/junk_mail_policy.ts
Normal file
88
worker/src/email/junk_mail_policy.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
type AuthenticationResult = {
|
||||
method: string,
|
||||
result: string,
|
||||
version?: string,
|
||||
}
|
||||
|
||||
const supportedResults: Record<string, ReadonlySet<string>> = {
|
||||
spf: new Set(["none", "neutral", "pass", "fail", "softfail", "policy", "temperror", "permerror"]),
|
||||
dkim: new Set(["none", "neutral", "pass", "fail", "policy", "temperror", "permerror"]),
|
||||
dmarc: new Set(["none", "pass", "fail", "temperror", "permerror"]),
|
||||
};
|
||||
const receivedSpfResults = new Set([
|
||||
"none", "neutral", "pass", "fail", "softfail", "temperror", "permerror",
|
||||
]);
|
||||
|
||||
const parseAuthenticationResults = (value: string): AuthenticationResult[] => {
|
||||
const results: AuthenticationResult[] = [];
|
||||
const pattern = /(?:^|;)[ \t]*([a-z][a-z0-9_-]*)(?:[ \t]*\/[ \t]*(\d+))?[ \t]*=[ \t]*([a-z][a-z0-9_-]*)/gi;
|
||||
|
||||
for (const match of value.matchAll(pattern)) {
|
||||
results.push({
|
||||
method: match[1].toLowerCase(),
|
||||
version: match[2],
|
||||
result: match[3].toLowerCase(),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const normalizeCheckName = (value: string): string => value.trim().toLowerCase();
|
||||
|
||||
const trackAuthenticationResult = (
|
||||
method: string,
|
||||
result: string | undefined,
|
||||
existing: Set<string>,
|
||||
passed: Set<string>,
|
||||
) => {
|
||||
if (!result || !supportedResults[method]?.has(result)) return;
|
||||
|
||||
// SPF and DKIM neutral are absent-equivalent under RFC 7208 and RFC 8601.
|
||||
if (result === "none" || ((method === "spf" || method === "dkim") && result === "neutral")) return;
|
||||
|
||||
existing.add(method);
|
||||
if (result === "pass") {
|
||||
passed.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
export const isJunkMailByHeaders = (
|
||||
headers: Record<string, string>[],
|
||||
checkListWhenExist: string[],
|
||||
forcePassList: string[],
|
||||
): boolean => {
|
||||
const passed = new Set<string>();
|
||||
const existing = new Set<string>();
|
||||
|
||||
for (const header of headers) {
|
||||
const key = header["key"]?.toLowerCase();
|
||||
const value = header["value"];
|
||||
if (!key || !value) continue;
|
||||
|
||||
if (key === "received-spf") {
|
||||
const result = value.match(/^[ \t]*([a-z][a-z0-9_-]*)/i)?.[1]?.toLowerCase();
|
||||
if (!result || !receivedSpfResults.has(result)) continue;
|
||||
trackAuthenticationResult("spf", result, existing, passed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key !== "authentication-results") continue;
|
||||
|
||||
for (const { method, result, version } of parseAuthenticationResults(value)) {
|
||||
if (method !== "spf" && method !== "dkim" && method !== "dmarc") continue;
|
||||
if (version && version !== "1") continue;
|
||||
trackAuthenticationResult(method, result, existing, passed);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkListWhenExist.some((checkName) => {
|
||||
const normalizedName = normalizeCheckName(checkName);
|
||||
return existing.has(normalizedName) && !passed.has(normalizedName);
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return forcePassList.some(
|
||||
(checkName) => !passed.has(normalizeCheckName(checkName))
|
||||
);
|
||||
}
|
||||
@@ -126,9 +126,9 @@ ENABLE_AUTO_REPLY = false
|
||||
# FRONTEND_URL = "https://xxxx.xxx"
|
||||
# Enable check junk mail
|
||||
# ENABLE_CHECK_JUNK_MAIL = false
|
||||
# junk mail check list, if status exists and status is not pass, will be marked as junk mail
|
||||
# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"]
|
||||
# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail
|
||||
# junk mail check list: reject registered failure/error results; none and SPF/DKIM neutral are treated as absent
|
||||
# JUNK_MAIL_CHECK_LIST = ["spf", "dkim", "dmarc"]
|
||||
# junk mail force pass list: every configured method must explicitly return pass
|
||||
# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"]
|
||||
# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing
|
||||
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
|
||||
|
||||
Reference in New Issue
Block a user