// // Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use regex::Regex; use std::panic; use std::sync::LazyLock; use tracing::error; /// Removes remote content references from HTML email body. /// /// Strips attributes that load content from http:// or https:// URLs, /// keeping data: URIs and cid: references intact. Does NOT affect /// navigation links (). pub fn block_remote_content(html: &str) -> String { let mut result = html.to_string(); // 1. Strip src, poster, data attributes with remote URLs. // These always load content regardless of the tag. static SRC_ATTR_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)\s+(src|poster|data)\s*=\s*["'][^"']*(?:https?://|//)[^"']*["']"#).unwrap() }); result = SRC_ATTR_RE.replace_all(&result, "").to_string(); // 2. Strip srcset attributes with remote URLs. static SRCSET_ATTR_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)\s+srcset\s*=\s*["'][^"']*(?:https?://|//)[^"']*["']"#).unwrap() }); result = SRCSET_ATTR_RE.replace_all(&result, "").to_string(); // 3. Strip href on tags (stylesheets), never links. static LINK_HREF_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)(]*)\s+href\s*=\s*["'][^"']*(?:https?://|//)[^"']*["']"#).unwrap() }); result = LINK_HREF_RE.replace_all(&result, "$1").to_string(); // 4. Strip CSS url() references with remote URLs in inline styles. static CSS_URL_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)url\(\s*["']?\s*(?:https?://|//)[^)"'\s]*\s*["']?\s*\)"#).unwrap() }); result = CSS_URL_RE.replace_all(&result, "").to_string(); // 5. Strip @import url(...) with remote URLs inside "#; let result = block_remote_content(html); assert!(!result.contains("https://fonts.example.com")); } #[test] fn strips_video_poster() { let html = r#""#; let result = block_remote_content(html); assert!(!result.contains("https://cdn.example.com")); } #[test] fn strips_srcset() { let html = r#""#; let result = block_remote_content(html); assert!(!result.contains("https://cdn.example.com")); } #[test] fn strips_body_background() { let html = r#""#; let result = block_remote_content(html); assert!(!result.contains("https://tracker.example.com")); assert!(result.contains(" ok Read more
"#; let result = block_remote_content(html); // Remote content gone assert!(!result.contains("spy.example.com")); assert!(!result.contains("tracker.example.com")); // Safe content preserved assert!(result.contains("data:image/png;base64,OK123")); assert!(result.contains(r#"href="https://example.com/read-more""#)); } } }