///|
pub struct SecuritySignal {
code : String
weight : Int
detail : String
} derive(Eq, Debug)
///|
pub fn security_signals(message : MailMessage) -> Array[SecuritySignal] {
let out : Array[SecuritySignal] = []
if has_auth_fail(message, "spf") {
out.push(signal("auth.spf.fail", 30, "SPF failed"))
}
if has_auth_fail(message, "dkim") {
out.push(signal("auth.dkim.fail", 30, "DKIM failed"))
}
if has_auth_fail(message, "dmarc") {
out.push(signal("auth.dmarc.fail", 40, "DMARC failed"))
}
if sender_domain(message) != "" &&
reply_to_domain(message) != "" &&
sender_domain(message) != reply_to_domain(message) {
out.push(
signal(
"reply_to.mismatch", 15, "Reply-To domain differs from From domain",
),
)
}
for url in message_urls(message) {
if url_looks_risky(url) {
out.push(signal("url.risky", 15, url))
}
}
for item in attachment_infos(message) {
if is_suspicious_filename(item.filename) {
out.push(signal("attachment.risky", 25, item.filename))
}
}
for finding in privacy_findings(message) {
if finding.code == "tracking.pixel" {
out.push(signal("privacy.tracking_pixel", 5, finding.detail))
}
}
out
}
///|
pub fn signal(code : String, weight : Int, detail : String) -> SecuritySignal {
{ code, weight, detail }
}
///|
pub fn security_score(message : MailMessage) -> Int {
let mut score = 0
for item in security_signals(message) {
score = score + item.weight
}
if score > 100 {
100
} else {
score
}
}
///|
pub fn is_high_risk(message : MailMessage) -> Bool {
security_score(message) >= 60
}
///|
pub fn reply_to_domain(message : MailMessage) -> String {
let reply = parse_address_list(header_value(message.headers, "Reply-To"))
if reply.length() > 0 {
mailbox_domain(reply[0].mailbox)
} else {
""
}
}
///|
fn url_looks_risky(url : String) -> Bool {
let lower = lower_ascii(url)
lower.contains("@") ||
lower.contains("%00") ||
lower.contains("xn--") ||
lower.contains("login") ||
lower.contains("verify") ||
lower.contains("reset")
}
///|
pub fn has_security_signal(
signals : Array[SecuritySignal],
code : String,
) -> Bool {
for item in signals {
if item.code == code {
return true
}
}
false
}
///|
pub fn security_summary(message : MailMessage) -> String {
let mut out = "score=" + security_score(message).to_string()
for item in security_signals(message) {
out = out + " " + item.code
}
out
}