use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SendOutcome {
pub code: u16,
pub server_message: String,
pub queue_id: Option<String>,
}
impl SendOutcome {
#[must_use]
pub fn new(code: u16, server_message: String) -> Self {
let queue_id = extract_queue_id(&server_message);
Self {
code,
server_message,
queue_id,
}
}
}
impl fmt::Display for SendOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.queue_id {
Some(qid) => write!(f, "{} (queue id {qid})", self.code),
None => write!(f, "{}: {}", self.code, self.server_message),
}
}
}
pub(crate) fn extract_queue_id(reply_text: &str) -> Option<String> {
if let Some(id) = scan_after(reply_text, "queued as ") {
return Some(id);
}
if let Some(id) = scan_after(reply_text, " id=") {
return Some(id);
}
if let Some(id) = scan_after(reply_text, " id ") {
return Some(id);
}
None
}
fn scan_after(text: &str, marker: &str) -> Option<String> {
let mut idx = 0;
while let Some(pos) = text[idx..].find(marker) {
let start = idx + pos + marker.len();
let token: String = text[start..]
.chars()
.take_while(|&c| is_queue_id_char(c))
.collect();
if token.len() >= 6 {
return Some(token);
}
idx = start;
if idx >= text.len() {
break;
}
}
None
}
fn is_queue_id_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_queue_id_postfix_format() {
assert_eq!(
extract_queue_id("2.0.0 Ok: queued as 4ABCDE12345"),
Some("4ABCDE12345".to_string())
);
}
#[test]
fn extract_queue_id_postfix_at_end_of_line() {
assert_eq!(
extract_queue_id("queued as ABCDEF1234"),
Some("ABCDEF1234".to_string())
);
}
#[test]
fn extract_queue_id_exim_format() {
assert_eq!(
extract_queue_id("OK id=1abcDe-0001Bc-Df"),
Some("1abcDe-0001Bc-Df".to_string())
);
}
#[test]
fn extract_queue_id_stalwart_id_space_format() {
assert_eq!(
extract_queue_id("2.0.0 Message queued with id ABCDEF123456"),
Some("ABCDEF123456".to_string())
);
}
#[test]
fn extract_queue_id_returns_none_when_no_pattern_matches() {
assert_eq!(extract_queue_id("Ok"), None);
assert_eq!(extract_queue_id("Message accepted for delivery"), None);
assert_eq!(extract_queue_id(""), None);
}
#[test]
fn extract_queue_id_rejects_too_short_tokens() {
assert_eq!(extract_queue_id("foo id 123"), None);
assert_eq!(extract_queue_id("foo queued as ABCDE"), None);
}
#[test]
fn extract_queue_id_with_trailing_punctuation() {
assert_eq!(
extract_queue_id("queued as 4ABCDE12345."),
Some("4ABCDE12345".to_string())
);
}
#[test]
fn extract_queue_id_handles_multiple_potential_matches() {
assert_eq!(
extract_queue_id("queued as FIRST123 id=SECOND456"),
Some("FIRST123".to_string())
);
}
#[test]
fn send_outcome_new_extracts_queue_id() {
let outcome = SendOutcome::new(250, "Ok: queued as 4ABCDE12345".to_string());
assert_eq!(outcome.code, 250);
assert_eq!(outcome.server_message, "Ok: queued as 4ABCDE12345");
assert_eq!(outcome.queue_id.as_deref(), Some("4ABCDE12345"));
}
#[test]
fn send_outcome_new_with_no_queue_id() {
let outcome = SendOutcome::new(250, "Message accepted for delivery".to_string());
assert_eq!(outcome.code, 250);
assert_eq!(outcome.queue_id, None);
}
#[test]
fn send_outcome_display_with_queue_id() {
let outcome = SendOutcome {
code: 250,
server_message: "Ok: queued as ABCDEF1234".to_string(),
queue_id: Some("ABCDEF1234".to_string()),
};
assert_eq!(format!("{outcome}"), "250 (queue id ABCDEF1234)");
}
#[test]
fn send_outcome_display_without_queue_id() {
let outcome = SendOutcome {
code: 250,
server_message: "Message accepted".to_string(),
queue_id: None,
};
assert_eq!(format!("{outcome}"), "250: Message accepted");
}
}