Reading a message fetches its remote images through this server, so the sender learns nothing about the reader. Quoting the same message into a reply fetched them directly: same pixel, same reader, but the request carried their IP and user agent -- exactly what the proxy withholds. A quote now proxies them the way the message view does. That alone would be wrong, because a proxied URL belongs to this deployment: sent unchanged it would reach the recipient as images only this server can serve, broken for them and a beacon back here. So buildEmailObject turns them back into the addresses they came from, beside the pass that restores images blocked under pr411 and the one that turns editor blob URLs into cid: references. Deployments with the proxy off are unaffected: the quote fetches directly, as reading does there. Three tests from pr411 asserted the address sat in src when images were allowed, which was the old behaviour; they now ask whether the draft fetches it at all, proxied or not. No new strings. (cherry picked from commit 23557a72a2a72088081792f8ea0cabedea4e7bcb)
66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
import { unproxiedImageUrl } from "@/lib/text/html";
|
|
import type { ImagePolicy } from "@/store/settings";
|
|
|
|
/**
|
|
* Whether a message's remote images may be fetched.
|
|
*
|
|
* The reader's decision, in one place, because the composer has to make the
|
|
* same one. Quoting a message into a reply renders it again — and a quote that
|
|
* fetched what the reader had declined would report the message read, and the
|
|
* address live, to whoever was counting. The tracking pixel does not care
|
|
* which window it loaded in.
|
|
*/
|
|
export function remoteImagesAllowed(opts: {
|
|
from: string | null | undefined;
|
|
policy: ImagePolicy;
|
|
trusted: string[];
|
|
inContacts: boolean;
|
|
/** The reader pressed "Show images" on this message. */
|
|
shown: boolean;
|
|
}): boolean {
|
|
if (opts.shown || opts.policy === "always") return true;
|
|
if (opts.trusted.includes((opts.from ?? "").toLowerCase())) return true;
|
|
return opts.policy === "contacts" && opts.inContacts;
|
|
}
|
|
|
|
/**
|
|
* Point proxied images back at their own addresses, on the way out.
|
|
*
|
|
* Reading a message fetches its remote images through this server, so the
|
|
* sender learns nothing about the reader. Those URLs belong to this
|
|
* deployment, so a quote that kept them would reach the recipient as images
|
|
* only this server can serve -- broken for them, and a beacon back here for
|
|
* anyone who could load them (#412).
|
|
*/
|
|
export function unproxyImages(html: string): string {
|
|
if (!html.includes("/api/image?url=")) return html;
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
for (const img of Array.from(doc.querySelectorAll("img[src]"))) {
|
|
const real = unproxiedImageUrl(img.getAttribute("src") ?? "");
|
|
if (real) img.setAttribute("src", real);
|
|
}
|
|
return doc.body.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Put back the addresses of images that were blocked when the message was
|
|
* quoted, on the way out.
|
|
*
|
|
* Blocking keeps the original URL on the element (`data-ihm-remote`), so
|
|
* nothing was lost by not fetching it. The copy that leaves here should be the
|
|
* quote as its sender wrote it: the recipient's client decides for itself
|
|
* whether to load those images, the same as it would have with any other
|
|
* client's reply.
|
|
*/
|
|
export function restoreBlockedImages(html: string): string {
|
|
if (!html.includes("data-ihm-blocked")) return html;
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
for (const img of Array.from(doc.querySelectorAll("img[data-ihm-blocked]"))) {
|
|
const url = img.getAttribute("data-ihm-remote");
|
|
if (url) img.setAttribute("src", url);
|
|
img.removeAttribute("data-ihm-blocked");
|
|
img.removeAttribute("data-ihm-remote");
|
|
}
|
|
return doc.body.innerHTML;
|
|
}
|