"use strict"; const NATIVE_HOST_ID = "it.codelinsoft.bastionguard.mail"; let configCache = null; let configCacheTime = 0; const CONFIG_CACHE_TTL = 60000; function t(key, substitutions = []) { return browser.i18n.getMessage(key, substitutions) || key; } async function ensureDefaultConfig() { try { const resp = await browser.runtime.sendNativeMessage(NATIVE_HOST_ID, { type: "get-config" }); if (resp?.ok && resp.config) { await browser.storage.local.set({ mailConfig: resp.config }); configCache = resp.config; configCacheTime = Date.now(); console.log("[BastionGuard]", t("config_loaded_native"), resp.config.profiles?.length ?? 0); return; } console.warn( "[BastionGuard]", t("config_failed"), resp?.error || resp?.error_code || "native host returned no config" ); } catch (e) { console.warn("[BastionGuard]", t("native_error"), e); } } // ============================================================ // Carica configurazione da native host // fallback: storage.local come cache // ============================================================ async function loadConfig() { const now = Date.now(); if (configCache && (now - configCacheTime) < CONFIG_CACHE_TTL) { return configCache; } try { const resp = await browser.runtime.sendNativeMessage(NATIVE_HOST_ID, { type: "get-config" }); if (resp?.ok && resp.config) { configCache = resp.config; configCacheTime = Date.now(); try { await browser.storage.local.set({ mailConfig: resp.config }); } catch (storageSetErr) { console.warn("[BastionGuard]", t("storage_error"), storageSetErr); } console.log("[BastionGuard]", t("config_loaded_native"), resp.config.profiles?.length ?? 0); return configCache; } console.warn( "[BastionGuard]", t("config_failed"), resp?.error || resp?.error_code || "native host returned no config" ); } catch (e) { console.warn("[BastionGuard]", t("native_error"), e); } try { const result = await browser.storage.local.get("mailConfig"); const cfg = result?.mailConfig || null; if (cfg && typeof cfg === "object") { configCache = cfg; configCacheTime = Date.now(); console.warn("[BastionGuard]", t("config_loaded_fallback"), cfg.profiles?.length ?? 0); return configCache; } } catch (e) { console.warn("[BastionGuard]", t("storage_error"), e); } return null; } // ============================================================ // Scansione email via native host // ============================================================ async function scanEmail(body, links, attachments = []) { try { const resp = await browser.runtime.sendNativeMessage(NATIVE_HOST_ID, { type: "scan-email", body, links, attachments }); return resp; } catch (e) { console.warn("[BastionGuard]", t("scan_error"), e); return { ok: false, clean: true, threats: [], phishing_links: [] }; } } // ============================================================ // Legge allegati dalla finestra di composizione // ============================================================ async function getComposeAttachments(tabId) { const out = []; try { const attachments = await browser.compose.listAttachments(tabId); for (const att of attachments) { try { const file = await browser.compose.getAttachmentFile(att.id); const buffer = await file.arrayBuffer(); const bytes = new Uint8Array(buffer); let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } out.push({ name: file.name || att.name || "attachment", content_type: file.type || "application/octet-stream", size: file.size || bytes.length, content_b64: btoa(binary) }); } catch (e) { console.warn("[BastionGuard] attachment read failed:", att?.name, e); } } } catch (e) { console.warn("[BastionGuard] listAttachments failed:", e); } return out; } // ============================================================ // Trova profilo SMTP // ============================================================ function findProfile(config, fromAddress) { if (!config?.profiles?.length) return null; const from = (fromAddress || "").toLowerCase().trim(); const fromDomain = from.includes("@") ? from.split("@")[1] : ""; for (const p of config.profiles) { if (p.match_from?.some(a => a.toLowerCase().trim() === from)) return p; if (p.match_from_domain?.some(d => d.toLowerCase().trim() === fromDomain)) return p; } if (config.default_profile_id) { const def = config.profiles.find(p => p.id === config.default_profile_id); if (def) return def; } return config.profiles[0] || null; } // ============================================================ // Estrai link // ============================================================ function extractLinks(text) { const urls = []; const re = /https?:\/\/[^\s"'<>]+/g; let m; while ((m = re.exec(text || "")) !== null) { let url = m[0].replace(/[.,;)]+$/, ""); if (!urls.includes(url)) { urls.push(url); } if (urls.length >= 10) { break; } } return urls; } // ============================================================ // Normalizzazione minacce / phishing // ============================================================ function formatThreat(threat) { if (!threat) return t("unknown_threat"); if (typeof threat === "string") { return threat; } if (typeof threat === "object") { const engine = threat.engine ? String(threat.engine).toUpperCase() : "ENGINE"; const source = threat.source === "attachment" && threat.attachment_name ? ` [${threat.attachment_name}]` : ""; const name = threat.name || threat.code || t("unknown_threat"); return `${engine}${source}: ${name}`; } return t("unknown_threat"); } function formatPhishingLabel(link) { if (!link) return "UNKNOWN"; if (link.classification) { return String(link.classification); } if (link.classification_code) { return String(link.classification_code).toUpperCase(); } return "UNKNOWN"; } function formatPhishingSeverity(link) { if (!link?.severity && !link?.severity_code) return ""; const sev = String(link.severity || link.severity_code).toUpperCase(); return ` (${sev})`; } // ============================================================ // Rimozione banner precedente // ============================================================ function stripExistingWarningBannerHtml(body) { if (!body) return body; return body.replace( /]*data-bastionguard-warning="1"[\s\S]*?<\/div>\s*/i, "" ); } function stripExistingWarningBannerText(body) { if (!body) return body; return body.replace( /\n?={8}\s*BASTIONGUARD WARNING\s*={8}[\s\S]*?={28}\n*/i, "\n" ); } // ============================================================ // Firma HTML // ============================================================ function buildSignatureHtml(profile) { const sig = profile?.signature; if (!sig) return ""; if (typeof sig.html === "string" && sig.html.trim()) { return `

${sig.html}`; } const name = sig.name || sig.display_name || ""; const title = sig.title || sig.job_title || ""; const company = sig.company || ""; const phone = sig.phone || ""; const email = sig.email || ""; const website = sig.website || ""; const logo = sig.logo || sig.logo_path || ""; const color = sig.color || "#c0392b"; if (!name && !company) return ""; let html = `

`; if (logo) { html += ``; } html += `
logo`; if (name) html += `${name}
`; if (title) html += `${title}
`; if (company) html += `${company}
`; const contacts = []; if (phone) contacts.push(`📞 ${phone}`); if (email) contacts.push(`✉ ${email}`); if (website) contacts.push(`🌐 ${website}`); if (contacts.length) { html += `${contacts.join("   ")}
`; } html += `${t("scanned_by")}`; html += `
`; return html; } // ============================================================ // Firma testo // ============================================================ function buildSignatureText(profile) { const sig = profile?.signature; const badge = t("scanned_by"); if (!sig) { return `\n\n-- \n${badge}`; } if (typeof sig.text === "string" && sig.text.trim()) { return `\n\n-- \n${sig.text}\n${badge}`; } const name = sig.name || sig.display_name || ""; const title = sig.title || sig.job_title || ""; const parts = [ name, title, sig.company, sig.phone ? `Tel: ${sig.phone}` : "", sig.email, sig.website, badge ].filter(Boolean); return `\n\n-- \n${parts.join("\n")}`; } // ============================================================ // Banner minaccia HTML // ============================================================ function buildThreatBannerHtml(threats, phishingLinks) { let html = `
⚠️ ${t("security_warning")}
${t("email_blocked")}
`; if (threats.length) { html += `
${t("threats_detected")}
    `; for (const threat of threats) { html += `
  • ${formatThreat(threat)}
  • `; } html += `
`; } if (phishingLinks.length) { html += `
${t("phishing_links")}
    `; for (const l of phishingLinks) { html += `
  • ${l.url} — ${formatPhishingLabel(l)}${formatPhishingSeverity(l)}
  • `; } html += `
`; } html += `
`; return html; } // ============================================================ // Banner minaccia testo // ============================================================ function buildThreatBannerText(threats, phishingLinks) { const lines = [ "", "======== BASTIONGUARD WARNING ========", `⚠️ ${t("security_warning")}`, t("email_blocked") ]; if (threats.length) { lines.push(t("threats_detected")); for (const threat of threats) { lines.push(` - ${formatThreat(threat)}`); } } if (phishingLinks.length) { lines.push(t("phishing_links")); for (const l of phishingLinks) { lines.push(` - ${l.url} — ${formatPhishingLabel(l)}${formatPhishingSeverity(l)}`); } } lines.push("============================"); lines.push(""); return lines.join("\n"); } // ============================================================ // Intercetta invio email // ============================================================ browser.compose.onBeforeSend.addListener(async (tab, details) => { try { const config = await loadConfig(); const fromAddress = typeof details.from === "object" ? (details.from?.email || "") : (details.from || ""); const profile = findProfile(config, fromAddress); console.log("[BastionGuard]", t("sending_from"), fromAddress); const cd = await browser.compose.getComposeDetails(tab.id); const isHtml = cd.isPlainText === false; const originalBody = isHtml ? (cd.body || "") : (cd.plainTextBody || ""); const body = isHtml ? stripExistingWarningBannerHtml(originalBody) : stripExistingWarningBannerText(originalBody); const scanEnabled = config?.scan_outgoing !== false; let scanResult = { clean: true, threats: [], phishing_links: [] }; if (scanEnabled) { const links = extractLinks(body); const attachments = await getComposeAttachments(tab.id); console.log("[BastionGuard]", t("scanning_links"), links.length); console.log("[BastionGuard] attachments found:", attachments.length); scanResult = await scanEmail(body, links, attachments); console.log("[BastionGuard] scan result:", scanResult); } if (!scanResult.clean) { const threats = scanResult.threats || []; const phishLinks = scanResult.phishing_links || []; console.warn("[BastionGuard]", t("threat_detected")); try { if (isHtml) { const banner = buildThreatBannerHtml(threats, phishLinks); const newBody = body.toLowerCase().includes("") ? body.replace(/]*>/i, `$&${banner}`) : banner + body; await browser.compose.setComposeDetails(tab.id, { body: newBody }); } else { const warningBanner = buildThreatBannerText(threats, phishLinks); await browser.compose.setComposeDetails(tab.id, { plainTextBody: warningBanner + body }); } } catch (updateErr) { console.error("[BastionGuard] setComposeDetails failed:", updateErr); } return { cancel: true }; } const injectSig = config?.inject_signature !== false; if (injectSig && !body.toLowerCase().includes("bastionguard")) { if (isHtml) { const sig = buildSignatureHtml(profile); if (sig) { const newBody = body.toLowerCase().includes("") ? body.replace(/<\/body>/i, `${sig}`) : body + sig; return { details: { body: newBody } }; } } else { return { details: { plainTextBody: body + buildSignatureText(profile) } }; } } return {}; } catch (err) { console.error("[BastionGuard]", t("send_error"), err); return {}; } }); // ============================================================ // Invalida cache quando cambia storage.local // ============================================================ browser.storage.onChanged.addListener((changes, area) => { if (area === "local" && changes.mailConfig) { configCache = null; configCacheTime = 0; console.log("[BastionGuard]", t("config_updated")); } }); ensureDefaultConfig().catch(err => { console.error("[BastionGuard]", "ensureDefaultConfig failed:", err); }); console.log("[BastionGuard]", t("extension_started"));