// mbauth 命令行入口。
//
// 判定逻辑全部在核心包;这里只负责参数、文件读写、渲染与退出码。
///|
const VERSION : String = "0.1.0"
///|
fn usage() -> String {
let s = StringBuilder()
s.write_string(
"mbauth \{VERSION} — 邮件身份验证(DKIM / SPF / DMARC)\n\n",
)
s.write_string("用法:\n")
s.write_string(" mbauth --ip [选项]\n\n")
s.write_string("选项:\n")
s.write_string(" --ip SMTP 对端 IP,SPF 必需\n")
s.write_string(
" --zone 离线 DNS zone(每行 `name TYPE value`)\n",
)
s.write_string(" --mail-from MAIL FROM 域,默认取 From 头\n")
s.write_string(" --helo HELO 名,供 SPF 宏使用\n")
s.write_string(" --format text | json | header,默认 text\n")
s.write_string(" --now 校验签名 x= 过期时间\n")
s.write_string(" --sample <0-99> DMARC pct 采样值\n")
s.write_string(" -v, --version 显示版本\n")
s.write_string(" -h, --help 显示本帮助\n\n")
s.write_string(
"退出码:0 通过,1 未通过,2 用法或读写错误\n",
)
s.to_string()
}
///|
fn parse_int(text : String) -> Int? {
if text.length() == 0 {
return None
}
let mut value = 0
for c in text {
if c < '0' || c > '9' {
return None
}
value = value * 10 + (c.to_int() - '0'.to_int())
}
Some(value)
}
///|
struct Options {
file : String
ip : String
zone : String
mail_from : String?
helo : String?
format : String
now : Int?
sample : Int?
}
///|
fn parse_args(args : Array[String]) -> Options? {
let mut file = ""
let mut ip = ""
let mut zone = ""
let mut mail_from : String? = None
let mut helo : String? = None
let mut format = "text"
let mut now : Int? = None
let mut sample : Int? = None
let mut i = 1
while i < args.length() {
let arg = args[i]
if arg == "--ip" && i + 1 < args.length() {
ip = args[i + 1]
i = i + 2
} else if arg == "--zone" && i + 1 < args.length() {
zone = args[i + 1]
i = i + 2
} else if arg == "--mail-from" && i + 1 < args.length() {
mail_from = Some(args[i + 1])
i = i + 2
} else if arg == "--helo" && i + 1 < args.length() {
helo = Some(args[i + 1])
i = i + 2
} else if arg == "--format" && i + 1 < args.length() {
format = args[i + 1]
i = i + 2
} else if arg == "--now" && i + 1 < args.length() {
now = parse_int(args[i + 1])
i = i + 2
} else if arg == "--sample" && i + 1 < args.length() {
sample = parse_int(args[i + 1])
i = i + 2
} else if arg == "-h" ||
arg == "--help" ||
arg == "-v" ||
arg == "--version" {
i = i + 1
} else if arg.has_prefix("--") {
return None
} else {
if file.length() == 0 {
file = arg
}
i = i + 1
}
}
if file.length() == 0 || ip.length() == 0 {
return None
}
Some({ file, ip, zone, mail_from, helo, format, now, sample, })
}
///|
fn read_bytes(path : String) -> Result[Bytes, String] {
Ok(@fs.read_file_to_bytes(path)) catch {
_ => Err("读取文件失败")
}
}
///|
fn read_text(path : String) -> Result[String, String] {
Ok(@fs.read_file_to_string(path)) catch {
_ => Err("读取文件失败")
}
}
///|
fn main {
let args = @env.args()
if args.contains("-h") || args.contains("--help") {
println(usage())
@sys.exit(0)
}
if args.contains("-v") || args.contains("--version") {
println("mbauth \{VERSION}")
@sys.exit(0)
}
let options = match parse_args(args) {
Some(value) => value
None => {
println(usage())
@sys.exit(2)
return
}
}
let raw = match read_bytes(options.file) {
Ok(value) => value
Err(message) => {
println("\{message}: \{options.file}")
@sys.exit(2)
return
}
}
let zone_text = if options.zone.length() == 0 {
""
} else {
match read_text(options.zone) {
Ok(value) => value
Err(message) => {
println("\{message}: \{options.zone}")
@sys.exit(2)
return
}
}
}
let zone = @Mr-Houjie/mbt-mailauth.Zone::from_text(zone_text)
let report = @Mr-Houjie/mbt-mailauth.verify_message(
zone,
raw,
options.ip,
options.mail_from.unwrap_or(""),
options.helo.unwrap_or(""),
options.now.unwrap_or(0),
options.sample.unwrap_or(-1),
)
match options.format {
"json" => println(@Mr-Houjie/mbt-mailauth.render_json(report))
"header" =>
println(
@Mr-Houjie/mbt-mailauth.authentication_results(report, "mbauth.local"),
)
_ => println(@Mr-Houjie/mbt-mailauth.render_text(report))
}
let failed = match report.dmarc {
Some(result) => result.status.label() == "fail"
None => false
}
@sys.exit(if failed { 1 } else { 0 })
}