Fetch List-Unsubscribe as URLs, the form RFC 8621 gives it #76
No files matched your search
@@ -201,7 +201,7 @@ export function addEmail(o: { from: [string, string]; to?: string; subject: stri
|
||||
attachments,
|
||||
bodyValues: { "1": { value: text, isEncodingProblem: false, isTruncated: false }, ...(o.html ? { "2": { value: o.styled ? STYLED_MARKETING_HTML : html, isEncodingProblem: false, isTruncated: false } } : {}) },
|
||||
bodyStructure: { partId: null, blobId: null, size: 0, type: "multipart/mixed", name: null, charset: null, disposition: null, cid: null, subParts: [{ partId: "1", blobId: textBlob, size: text.length, type: "text/plain", name: null, charset: "utf-8", disposition: null, cid: null }, ...(o.html ? [{ partId: "2", blobId: htmlBlob, size: (o.styled ? STYLED_MARKETING_HTML : html).length, type: "text/html", name: null, charset: "utf-8", disposition: null, cid: null }] : []), ...attachments] },
|
||||
"header:List-Unsubscribe:asText": o.from[1].includes("newsletter") ? "<mailto:[email protected]?subject=unsubscribe>, <https://newsletter.example/unsub>" : null,
|
||||
"header:List-Unsubscribe:asURLs": o.from[1].includes("newsletter") ? ["mailto:[email protected]?subject=unsubscribe", "https://newsletter.example/unsub"] : null,
|
||||
"header:X-Priority:asText": o.subject.startsWith("Security") ? "1 (Highest)" : null,
|
||||
// Stalwart's spam filter writes the SpamAssassin-shaped set at delivery, so
|
||||
// delivered mail carries it and mail this account wrote does not.
|
||||
|
||||
@@ -254,7 +254,8 @@ export interface Email {
|
||||
attachments?: EmailBodyPart[];
|
||||
headers?: EmailHeader[];
|
||||
// convenience header fetches
|
||||
"header:List-Unsubscribe:asText"?: string | null;
|
||||
/** RFC 2369 defines List-Unsubscribe, so asText is not a form it has (RFC 8621 §4.1.2.3). */
|
||||
"header:List-Unsubscribe:asURLs"?: string[] | null;
|
||||
"header:List-Unsubscribe-Post:asText"?: string | null;
|
||||
"header:List-Id:asText"?: string | null;
|
||||
"header:Disposition-Notification-To:asAddresses"?: EmailAddress[] | null;
|
||||
@@ -263,9 +264,7 @@ export interface Email {
|
||||
"header:Auto-Submitted:asText"?: string | null;
|
||||
/** Bulk/list mail marks itself here; read receipts for it only confirm the address. */
|
||||
"header:Precedence:asText"?: string | null;
|
||||
"header:Return-Path:asText"?: string | null;
|
||||
"header:Authentication-Results:asText"?: string | null;
|
||||
"header:Received:asText:all"?: string[] | null;
|
||||
"header:X-Spam-Status:asText"?: string | null;
|
||||
"header:X-Spam-Result:asText"?: string | null;
|
||||
/** inbuxa: the language model's opinion, when AI spam classification is on (lib/llmOpinion). */
|
||||
|
||||
@@ -58,7 +58,7 @@ export const FULL_PROPS = [
|
||||
"textBody",
|
||||
"htmlBody",
|
||||
"attachments",
|
||||
"header:List-Unsubscribe:asText",
|
||||
"header:List-Unsubscribe:asURLs",
|
||||
"header:List-Unsubscribe-Post:asText",
|
||||
"header:List-Id:asText",
|
||||
"header:Disposition-Notification-To:asAddresses",
|
||||
|
||||
@@ -192,7 +192,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
const attachments = useMemo(() => (e.attachments ?? []).filter((a) => !(a.cid && a.disposition === "inline" && a.type.startsWith("image/") && htmlRaw?.includes(`cid:${a.cid}`))), [e.attachments, htmlRaw]);
|
||||
const icsPart = useMemo(() => findPart(e.bodyStructure, (p) => p.type === "text/calendar" || (p.name ?? "").toLowerCase().endsWith(".ics")), [e.bodyStructure]);
|
||||
const vcfParts = useMemo(() => (e.attachments ?? []).filter((p) => p.type === "text/vcard" || p.type === "text/x-vcard" || (p.name ?? "").toLowerCase().endsWith(".vcf")), [e.attachments]);
|
||||
const unsubscribe = e["header:List-Unsubscribe:asText"];
|
||||
const unsubscribe = e["header:List-Unsubscribe:asURLs"];
|
||||
const isHighPriority = /^[12]/.test(e["header:X-Priority:asText"] ?? "") || /high/i.test(e["header:Importance:asText"] ?? "");
|
||||
const receiptRequested = Boolean(e["header:Disposition-Notification-To:asAddresses"]?.length);
|
||||
const authFailed = /\b(dkim|spf|dmarc)=fail\b/i.test(e["header:Authentication-Results:asText"] ?? "");
|
||||
@@ -250,8 +250,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
};
|
||||
|
||||
const onUnsubscribe = async () => {
|
||||
if (!unsubscribe) return;
|
||||
const urls = [...unsubscribe.matchAll(/<([^>]+)>/g)].map((m) => m[1]!);
|
||||
const urls = unsubscribe ?? [];
|
||||
const mailto = urls.find((u) => u.startsWith("mailto:"));
|
||||
const http = urls.find((u) => /^https?:/i.test(u));
|
||||
if (mailto) {
|
||||
@@ -465,12 +464,12 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
{showHtml && rendered ? <HtmlBody html={rendered.html} bodyStyle={rendered.bodyStyle} themed={themed} forced={forced} onShowImages={showImages} onFollowLink={linkGuard} /> : <TextBody text={textRaw ?? ""} onFollowLink={linkGuard} />}
|
||||
</div>
|
||||
{attachments.length > 0 && <AttachmentList attachments={attachments} accountId={accountId} email={e} />}
|
||||
{unsubscribe && (
|
||||
{unsubscribe?.length ? (
|
||||
<div className="unsubscribe-row">
|
||||
<span>{translate("This looks like a mailing list.")}</span>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => void onUnsubscribe()}>{translate("Unsubscribe")}</button>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{addrMenu.node}
|
||||
|
||||
Reference in new issue
Block a user