///|
pub(all) enum Severity {
Info
Warning
Error
} derive(Eq, Debug)
///|
pub struct LintFinding {
severity : Severity
code : String
message : String
} derive(Eq, Debug)
///|
pub fn lint_message(message : MailMessage) -> Array[LintFinding] {
let out : Array[LintFinding] = []
if message.from.length() == 0 {
out.push(
finding(
Error,
"header.from.missing",
"From header is required for most mailbox workflows",
),
)
}
if message.to.length() == 0 && message.cc.length() == 0 {
out.push(
finding(
Warning,
"recipient.missing",
"message has neither To nor Cc recipients",
),
)
}
if message.subject == "" {
out.push(finding(Warning, "subject.empty", "message subject is empty"))
}
if plain_text(message) == "" && html_text(message) == "" {
out.push(
finding(
Warning,
"body.empty",
"message has no readable text or html body",
),
)
}
lint_part(message.root, out)
out
}
///|
fn lint_part(part : MailPart, out : Array[LintFinding]) -> Unit {
if part.content_type.media_type == "multipart" &&
part.content_type.boundary == "" {
out.push(
finding(
Error,
"mime.boundary.missing",
"multipart part is missing boundary parameter",
),
)
}
if is_attachment(part) && part.filename == "" {
out.push(
finding(
Warning,
"attachment.filename.missing",
"attachment has no filename",
),
)
}
if is_attachment(part) && is_suspicious_filename(part.filename) {
out.push(
finding(
Warning,
"attachment.filename.suspicious",
"attachment filename has a risky extension",
),
)
}
for child in part.children {
lint_part(child, out)
}
}
///|
pub fn finding(
severity : Severity,
code : String,
message : String,
) -> LintFinding {
{ severity, code, message }
}
///|
pub fn has_lint_code(findings : Array[LintFinding], code : String) -> Bool {
for item in findings {
if item.code == code {
return true
}
}
false
}
///|
pub fn warning_count(findings : Array[LintFinding]) -> Int {
let mut count = 0
for item in findings {
if item.severity == Warning {
count = count + 1
}
}
count
}
///|
pub fn error_count(findings : Array[LintFinding]) -> Int {
let mut count = 0
for item in findings {
if item.severity == Error {
count = count + 1
}
}
count
}
///|
pub fn is_suspicious_filename(filename : String) -> Bool {
let lower = lower_ascii(filename)
ends_with(lower, ".exe") ||
ends_with(lower, ".bat") ||
ends_with(lower, ".cmd") ||
ends_with(lower, ".scr") ||
ends_with(lower, ".js") ||
ends_with(lower, ".vbs")
}
///|
pub fn ends_with(input : String, suffix : String) -> Bool {
if suffix.length() > input.length() {
false
} else {
slice(input, input.length() - suffix.length(), input.length()) == suffix
}
}