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,145 @@
use mail_auth::spf::verify::SpfParameters;
use mail_auth::{MessageAuthenticator, SpfResult as MailAuthSpfResult};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use crate::error::Result;
/// SPF verification result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpfResult {
/// The SPF result: "pass", "fail", "softfail", "neutral", "temperror", "permerror", "none".
pub result: String,
/// The domain that was checked.
pub domain: String,
/// The IP address that was checked.
pub ip: String,
/// Optional explanation string from the SPF record.
pub explanation: Option<String>,
}
impl SpfResult {
/// Whether the SPF check passed.
pub fn passed(&self) -> bool {
self.result == "pass"
}
}
/// Check SPF for a given sender IP, HELO domain, and MAIL FROM address.
///
/// * `ip` - The connecting client's IP address
/// * `helo_domain` - The domain from the SMTP EHLO/HELO command
/// * `host_domain` - Your receiving server's hostname
/// * `mail_from` - The full MAIL FROM address (e.g., "sender@example.com")
pub async fn check_spf(
ip: IpAddr,
helo_domain: &str,
host_domain: &str,
mail_from: &str,
authenticator: &MessageAuthenticator,
) -> Result<SpfResult> {
let output = authenticator
.verify_spf(SpfParameters::verify_mail_from(
ip,
helo_domain,
host_domain,
mail_from,
))
.await;
let result_str = match output.result() {
MailAuthSpfResult::Pass => "pass",
MailAuthSpfResult::Fail => "fail",
MailAuthSpfResult::SoftFail => "softfail",
MailAuthSpfResult::Neutral => "neutral",
MailAuthSpfResult::TempError => "temperror",
MailAuthSpfResult::PermError => "permerror",
MailAuthSpfResult::None => "none",
};
Ok(SpfResult {
result: result_str.to_string(),
domain: output.domain().to_string(),
ip: ip.to_string(),
explanation: output.explanation().map(|s| s.to_string()),
})
}
/// Check SPF for the EHLO identity (before MAIL FROM).
pub async fn check_spf_ehlo(
ip: IpAddr,
helo_domain: &str,
host_domain: &str,
authenticator: &MessageAuthenticator,
) -> Result<SpfResult> {
let output = authenticator
.verify_spf(SpfParameters::verify_ehlo(ip, helo_domain, host_domain))
.await;
let result_str = match output.result() {
MailAuthSpfResult::Pass => "pass",
MailAuthSpfResult::Fail => "fail",
MailAuthSpfResult::SoftFail => "softfail",
MailAuthSpfResult::Neutral => "neutral",
MailAuthSpfResult::TempError => "temperror",
MailAuthSpfResult::PermError => "permerror",
MailAuthSpfResult::None => "none",
};
Ok(SpfResult {
result: result_str.to_string(),
domain: helo_domain.to_string(),
ip: ip.to_string(),
explanation: output.explanation().map(|s| s.to_string()),
})
}
/// Build a Received-SPF header value.
pub fn received_spf_header(result: &SpfResult) -> String {
format!(
"{} (domain of {} designates {} as permitted sender) receiver={}; client-ip={};",
result.result,
result.domain,
result.ip,
result.domain,
result.ip,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spf_result_passed() {
let result = SpfResult {
result: "pass".to_string(),
domain: "example.com".to_string(),
ip: "1.2.3.4".to_string(),
explanation: None,
};
assert!(result.passed());
let result = SpfResult {
result: "fail".to_string(),
domain: "example.com".to_string(),
ip: "1.2.3.4".to_string(),
explanation: None,
};
assert!(!result.passed());
}
#[test]
fn test_received_spf_header() {
let result = SpfResult {
result: "pass".to_string(),
domain: "example.com".to_string(),
ip: "1.2.3.4".to_string(),
explanation: None,
};
let header = received_spf_header(&result);
assert!(header.contains("pass"));
assert!(header.contains("example.com"));
assert!(header.contains("1.2.3.4"));
}
}