BastionGuard/data/extension/bastionguard-tb-extension/background.js

510 lines
16 KiB
JavaScript

"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(
/<div[^>]*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 `<br><br>${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 = `<br><br><table style="border-top:2px solid ${color};padding-top:8px;font-family:Arial,sans-serif;font-size:12px;color:#333;"><tr>`;
if (logo) {
html += `<td style="padding-right:12px;vertical-align:middle;"><img src="${logo}" alt="logo" width="64" style="display:block;border-radius:4px;"/></td>`;
}
html += `<td style="vertical-align:top;">`;
if (name) html += `<strong style="font-size:13px;">${name}</strong><br>`;
if (title) html += `<span style="color:#555;">${title}</span><br>`;
if (company) html += `<span style="color:${color};font-weight:bold;">${company}</span><br>`;
const contacts = [];
if (phone) contacts.push(`📞 ${phone}`);
if (email) contacts.push(`✉ <a href="mailto:${email}" style="color:#555;">${email}</a>`);
if (website) contacts.push(`🌐 <a href="${website}" style="color:#555;">${website}</a>`);
if (contacts.length) {
html += `<span style="color:#888;font-size:10px;">${contacts.join(" &nbsp; ")}</span><br>`;
}
html += `<span style="color:#aaa;font-size:10px;margin-top:4px;display:block;">${t("scanned_by")}</span>`;
html += `</td></tr></table>`;
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 = `
<div data-bastionguard-warning="1" style="background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:12px;margin-bottom:12px;font-family:Arial,sans-serif;color:#333;">
<div style="font-weight:bold;color:#856404;">⚠️ ${t("security_warning")}</div>
<div style="margin-top:6px;">${t("email_blocked")}</div>
`;
if (threats.length) {
html += `<div style="margin-top:8px;"><strong>${t("threats_detected")}</strong><ul>`;
for (const threat of threats) {
html += `<li>${formatThreat(threat)}</li>`;
}
html += `</ul></div>`;
}
if (phishingLinks.length) {
html += `<div style="margin-top:8px;"><strong>${t("phishing_links")}</strong><ul>`;
for (const l of phishingLinks) {
html += `<li><code>${l.url}</code> — ${formatPhishingLabel(l)}${formatPhishingSeverity(l)}</li>`;
}
html += `</ul></div>`;
}
html += `</div>`;
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>")
? body.replace(/<body[^>]*>/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>")
? body.replace(/<\/body>/i, `${sig}</body>`)
: 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"));