510 lines
16 KiB
JavaScript
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(" ")}</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"));
|