///|
/// One recorded command -> response interaction of an SMTP session.
pub struct SmtpStep {
command : String
code : Int
text : String
duration_ms : Int64
} derive(Eq, Debug)
///|
/// Redact a command for the trace: AUTH payloads (which contain credentials)
/// are never recorded (MM_SMTP_001 security model, section 15).
pub fn redact_command(command : String) -> String {
let upper = command.to_upper()
if upper.has_prefix("AUTH PLAIN ") {
"AUTH PLAIN [REDACTED]"
} else if upper == "AUTH LOGIN" || upper.has_prefix("AUTH LOGIN ") {
"AUTH LOGIN"
} else if upper == "AUTH CRAM-MD5" || upper.has_prefix("AUTH CRAM-MD5 ") {
"AUTH CRAM-MD5"
} else {
command
}
}
///|
/// A full diagnostic report of an SMTP session: every step, the negotiated
/// capabilities, the auth mechanism used, and any diagnostics accumulated.
pub struct SmtpTrace {
steps : Array[SmtpStep]
mut capabilities : SmtpCaps
mut auth_used : String
mut final_code : Int
diagnostics : Array[String]
} derive(Eq, Debug)
///|
pub fn SmtpTrace::empty() -> SmtpTrace {
{
steps: [],
capabilities: SmtpCaps::empty(),
auth_used: "",
final_code: 0,
diagnostics: [],
}
}
///|
pub fn SmtpTrace::steps(self : SmtpTrace) -> Array[SmtpStep] {
self.steps
}
///|
pub fn SmtpTrace::capabilities(self : SmtpTrace) -> SmtpCaps {
self.capabilities
}
///|
pub fn SmtpTrace::auth_used(self : SmtpTrace) -> String {
self.auth_used
}
///|
pub fn SmtpTrace::final_code(self : SmtpTrace) -> Int {
self.final_code
}
///|
pub fn SmtpTrace::diagnostics(self : SmtpTrace) -> Array[String] {
self.diagnostics
}
///|
pub fn SmtpTrace::add_step(
self : SmtpTrace,
command : String,
reply : ServerReply,
duration_ms : Int64,
) -> Unit {
self.steps.push({
command: redact_command(command),
code: reply.code,
text: reply.text,
duration_ms,
})
}
///|
pub fn SmtpTrace::add_diagnostic(self : SmtpTrace, message : String) -> Unit {
self.diagnostics.push(message)
}
///|
/// Render the trace as a readable transcript, e.g. for `moonmail explain`.
pub fn SmtpTrace::render(self : SmtpTrace) -> String {
let buf = Buffer::Buffer()
buf.write_string_utf16le("SmtpTrace {\n")
buf.write_string_utf16le(
" capabilities: AUTH=[\{self.capabilities.auth.join(",")}] 8BITMIME=\{self.capabilities.has_8bitmime} SMTPUTF8=\{self.capabilities.has_smtputf8}\n",
)
if self.auth_used != "" {
buf.write_string_utf16le(" auth: \{self.auth_used}\n")
}
for step in self.steps {
buf.write_string_utf16le(" \{step.command} -> \{step.code} \{step.text}\n")
}
for d in self.diagnostics {
buf.write_string_utf16le(" diag: \{d}\n")
}
buf.write_string_utf16le(" final_code: \{self.final_code}\n")
buf.write_string_utf16le("}")
buf.to_string()
}