feat(rust): implement mailer-core and mailer-security crates with CLI
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s

Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).

mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.

mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.

mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
This commit is contained in:
2026-02-10 16:06:04 +00:00
parent 91b49182bb
commit 199b9b79d2
15 changed files with 3113 additions and 261 deletions

View File

@@ -0,0 +1,178 @@
use regex::Regex;
use std::sync::LazyLock;
/// Basic email format regex — covers the vast majority of valid email addresses.
/// Does NOT attempt to match the full RFC 5321 grammar (which is impractical via regex).
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?i)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
)
.expect("invalid email regex")
});
/// Check whether an email address has valid syntax.
pub fn is_valid_email_format(email: &str) -> bool {
let email = email.trim();
if email.is_empty() || email.len() > 254 {
return false;
}
let parts: Vec<&str> = email.rsplitn(2, '@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[1];
let domain = parts[0];
// Local part max 64 chars
if local.is_empty() || local.len() > 64 {
return false;
}
// Domain must have at least one dot (TLD only not valid for email)
if !domain.contains('.') {
return false;
}
EMAIL_REGEX.is_match(email)
}
/// Email validation result with scoring.
#[derive(Debug, Clone)]
pub struct EmailValidationResult {
pub is_valid: bool,
pub format_valid: bool,
pub score: f64,
pub error_message: Option<String>,
}
/// Validate an email address (synchronous, format-only).
/// DNS-based validation (MX records, disposable domains) would require async and is
/// intended for the N-API bridge layer where the TypeScript side already has DNS access.
pub fn validate_email(email: &str) -> EmailValidationResult {
let format_valid = is_valid_email_format(email);
if !format_valid {
return EmailValidationResult {
is_valid: false,
format_valid: false,
score: 0.0,
error_message: Some(format!("Invalid email format: {}", email)),
};
}
// Role account detection (weight 0.1 penalty)
let local = email.split('@').next().unwrap_or("");
let is_role = is_role_account(local);
// Score: format (0.4) + assumed-mx (0.3) + assumed-not-disposable (0.2) + role (0.1)
let mut score = 0.4 + 0.3 + 0.2; // format + mx + not-disposable
if !is_role {
score += 0.1;
}
EmailValidationResult {
is_valid: score >= 0.7,
format_valid: true,
score,
error_message: None,
}
}
/// Check if a local part is a common role account.
fn is_role_account(local: &str) -> bool {
const ROLE_ACCOUNTS: &[&str] = &[
"abuse",
"admin",
"administrator",
"billing",
"compliance",
"devnull",
"dns",
"ftp",
"hostmaster",
"info",
"inoc",
"ispfeedback",
"ispsupport",
"list",
"list-request",
"maildaemon",
"mailer-daemon",
"mailerdaemon",
"marketing",
"noc",
"no-reply",
"noreply",
"null",
"phish",
"phishing",
"postmaster",
"privacy",
"registrar",
"root",
"sales",
"security",
"spam",
"support",
"sysadmin",
"tech",
"undisclosed-recipients",
"unsubscribe",
"usenet",
"uucp",
"webmaster",
"www",
];
let lower = local.to_lowercase();
ROLE_ACCOUNTS.contains(&lower.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_emails() {
assert!(is_valid_email_format("user@example.com"));
assert!(is_valid_email_format("first.last@example.com"));
assert!(is_valid_email_format("user+tag@example.com"));
assert!(is_valid_email_format("user@sub.domain.example.com"));
}
#[test]
fn test_invalid_emails() {
assert!(!is_valid_email_format(""));
assert!(!is_valid_email_format("@"));
assert!(!is_valid_email_format("user@"));
assert!(!is_valid_email_format("@domain.com"));
assert!(!is_valid_email_format("user@domain")); // no TLD
assert!(!is_valid_email_format("user @domain.com")); // space
assert!(!is_valid_email_format("user@.com")); // leading dot
}
#[test]
fn test_validate_email_scoring() {
let result = validate_email("user@example.com");
assert!(result.is_valid);
assert!(result.score >= 0.9);
let result = validate_email("postmaster@example.com");
assert!(result.is_valid);
assert!(result.score >= 0.7);
assert!(result.score < 1.0); // role account penalty
let result = validate_email("not-an-email");
assert!(!result.is_valid);
assert_eq!(result.score, 0.0);
}
#[test]
fn test_role_accounts() {
assert!(is_role_account("postmaster"));
assert!(is_role_account("abuse"));
assert!(is_role_account("noreply"));
assert!(!is_role_account("john"));
assert!(!is_role_account("alice"));
}
}