use super::*;
#[test]
fn smtp_types_module_and_core_types_carry_rfc_section_citations() {
let source = include_str!("mod.rs")
.split("#[cfg(test)]")
.next()
.unwrap_or("");
for required in [
"//! - RFC 5321 (SMTP)",
"//! - RFC 2033 (LMTP)",
"//! - RFC 2034 (Enhanced Status Codes)",
"//! - RFC 4954 (SMTP AUTH)",
"/// A parsed SMTP server response (RFC 5321 Section 4.2).",
"/// Enhanced status code (RFC 1893 Section 2 / RFC 2034 Section 3).",
"/// SMTP server extension capabilities, parsed from EHLO response",
"RFC 5321 Section 4.1.1.1",
"/// SMTP authentication mechanism (RFC 4954 Section 3 / RFC 4422 Section 3.1).",
"/// Server capabilities parsed from EHLO response",
] {
assert!(
source.contains(required),
"smtp::types must cite RFC sections for module docs and core protocol types; missing: {required}"
);
}
}
#[test]
fn smtp_response_classification() {
let ok = SmtpResponse {
code: 250,
enhanced_code: None,
lines: vec!["OK".into()],
};
assert!(ok.is_success());
assert!(!ok.is_transient_error());
assert!(!ok.is_permanent_error());
let transient = SmtpResponse {
code: 421,
enhanced_code: None,
lines: vec!["Try again later".into()],
};
assert!(transient.is_transient_error());
assert!(!transient.is_success());
let permanent = SmtpResponse {
code: 550,
enhanced_code: None,
lines: vec!["Mailbox not found".into()],
};
assert!(permanent.is_permanent_error());
assert!(!permanent.is_transient_error());
}
#[test]
fn smtp_response_text() {
let resp = SmtpResponse {
code: 250,
enhanced_code: None,
lines: vec!["line1".into(), "line2".into()],
};
assert_eq!(resp.text(), "line1\nline2");
}
#[test]
fn server_capabilities_starttls() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::StartTls, SmtpExtension::Pipelining],
};
assert!(caps.supports_starttls());
assert!(!caps.supports_chunking());
}
#[test]
fn server_capabilities_auth() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Auth(vec![
AuthMechanism::Plain,
AuthMechanism::XOAuth2,
])],
};
assert!(caps.supports_auth(&AuthMechanism::Plain));
assert!(caps.supports_auth(&AuthMechanism::XOAuth2));
assert!(!caps.supports_auth(&AuthMechanism::Other("CRAM-MD5".into())));
}
#[test]
fn intermediate_response() {
let resp = SmtpResponse {
code: 354,
enhanced_code: None,
lines: vec!["Start mail input".into()],
};
assert!(resp.is_intermediate());
assert!(!resp.is_success());
}
#[test]
fn supports_8bitmime() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::EightBitMime],
};
assert!(caps.supports_8bitmime());
let empty = ServerCapabilities::default();
assert!(!empty.supports_8bitmime());
}
#[test]
fn supports_binarymime() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::BinaryMime],
};
assert!(caps.supports_binarymime());
let empty = ServerCapabilities::default();
assert!(!empty.supports_binarymime());
}
#[test]
fn supports_8bit_or_binary() {
let with_8bit = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::EightBitMime],
};
assert!(with_8bit.supports_8bit_or_binary());
let with_binary = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::BinaryMime],
};
assert!(with_binary.supports_8bit_or_binary());
let with_both = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::EightBitMime, SmtpExtension::BinaryMime],
};
assert!(with_both.supports_8bit_or_binary());
let empty = ServerCapabilities::default();
assert!(!empty.supports_8bit_or_binary());
}
#[test]
fn supports_pipelining() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Pipelining],
};
assert!(caps.supports_pipelining());
let empty = ServerCapabilities::default();
assert!(!empty.supports_pipelining());
}
#[test]
fn supports_smtputf8() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::SmtpUtf8],
};
assert!(caps.supports_smtputf8());
let empty = ServerCapabilities::default();
assert!(!empty.supports_smtputf8());
}
#[test]
fn supports_auth_case_insensitive_other_mechanism() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Auth(vec![
AuthMechanism::Other("LOGIN".into()),
AuthMechanism::Other("CRAM-MD5".into()),
])],
};
assert!(
caps.supports_auth(&AuthMechanism::Other("LOGIN".into())),
"exact case must match"
);
assert!(
caps.supports_auth(&AuthMechanism::Other("login".into())),
"RFC 4954 Section 3: mechanism names are case-insensitive; \
'login' must match stored 'LOGIN'"
);
assert!(
caps.supports_auth(&AuthMechanism::Other("Login".into())),
"RFC 4954 Section 3: mixed-case 'Login' must match stored 'LOGIN'"
);
assert!(
caps.supports_auth(&AuthMechanism::Other("cram-md5".into())),
"RFC 4954 Section 3: 'cram-md5' must match stored 'CRAM-MD5'"
);
assert!(
!caps.supports_auth(&AuthMechanism::Other("NTLM".into())),
"non-existent mechanism must not match"
);
}
#[test]
fn eq_mechanism_cross_variant_other_plain() {
assert!(
AuthMechanism::Other("PLAIN".into()).eq_mechanism(&AuthMechanism::Plain),
"Other(\"PLAIN\") must match Plain (RFC 4954 Section 3)"
);
assert!(
AuthMechanism::Plain.eq_mechanism(&AuthMechanism::Other("plain".into())),
"Plain must match Other(\"plain\") (RFC 4954 Section 3)"
);
}
#[test]
fn auth_mechanism_public_equality_is_case_insensitive() {
assert_eq!(
AuthMechanism::Other("plain".into()),
AuthMechanism::Plain,
"public AuthMechanism equality must treat SASL names case-insensitively"
);
}
#[test]
fn auth_mechanism_public_hash_is_case_insensitive() {
let mut set = std::collections::HashSet::new();
set.insert(AuthMechanism::Other("LOGIN".into()));
set.insert(AuthMechanism::Login);
assert_eq!(
set.len(),
1,
"equivalent SASL mechanisms must hash identically for HashSet/HashMap use"
);
}
#[test]
fn eq_mechanism_cross_variant_other_xoauth2() {
assert!(
AuthMechanism::Other("XOAUTH2".into()).eq_mechanism(&AuthMechanism::XOAuth2),
"Other(\"XOAUTH2\") must match XOAuth2"
);
assert!(
AuthMechanism::XOAuth2.eq_mechanism(&AuthMechanism::Other("xoauth2".into())),
"XOAuth2 must match Other(\"xoauth2\")"
);
}
#[test]
fn supports_auth_cross_variant_other_plain() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Auth(vec![AuthMechanism::Plain])],
};
assert!(
caps.supports_auth(&AuthMechanism::Other("PLAIN".into())),
"RFC 4954 Section 3: Other(\"PLAIN\") query must match \
stored Plain variant"
);
assert!(
caps.supports_auth(&AuthMechanism::Other("plain".into())),
"RFC 4954 Section 3: Other(\"plain\") query must match \
stored Plain variant (case-insensitive)"
);
}
#[test]
fn supports_chunking() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Chunking],
};
assert!(caps.supports_chunking());
let empty = ServerCapabilities::default();
assert!(!empty.supports_chunking());
}
#[test]
fn supports_size() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Size(Some(10_485_760))],
};
assert!(caps.supports_size());
let caps_no_limit = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Size(None)],
};
assert!(caps_no_limit.supports_size());
let empty = ServerCapabilities::default();
assert!(!empty.supports_size());
}
#[test]
fn supports_sasl_ir() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::SaslIr],
};
assert!(caps.supports_sasl_ir());
let empty = ServerCapabilities::default();
assert!(!empty.supports_sasl_ir());
}
#[test]
fn supports_enhanced_status_codes() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::EnhancedStatusCodes],
};
assert!(caps.supports_enhanced_status_codes());
let empty = ServerCapabilities::default();
assert!(!empty.supports_enhanced_status_codes());
}
#[test]
fn size_limit_with_value() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Size(Some(10_485_760))],
};
assert_eq!(caps.size_limit(), Some(10_485_760));
}
#[test]
fn size_limit_without_value() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Size(None)],
};
assert_eq!(caps.size_limit(), None);
}
#[test]
fn size_limit_not_advertised() {
let empty = ServerCapabilities::default();
assert_eq!(empty.size_limit(), None);
}
#[test]
fn supports_dsn() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Dsn],
};
assert!(caps.supports_dsn());
let empty = ServerCapabilities::default();
assert!(!empty.supports_dsn());
}
#[test]
fn supports_requiretls() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::RequireTls],
};
assert!(caps.supports_requiretls());
let empty = ServerCapabilities::default();
assert!(!empty.supports_requiretls());
}
#[test]
fn supports_auth_login() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Auth(vec![
AuthMechanism::Plain,
AuthMechanism::Login,
])],
};
assert!(caps.supports_auth(&AuthMechanism::Login));
assert!(caps.supports_auth(&AuthMechanism::Plain));
}
#[test]
fn eq_mechanism_login_identity() {
assert!(AuthMechanism::Login.eq_mechanism(&AuthMechanism::Login));
}
#[test]
fn eq_mechanism_cross_variant_other_login() {
assert!(
AuthMechanism::Other("LOGIN".into()).eq_mechanism(&AuthMechanism::Login),
"Other(\"LOGIN\") must match Login"
);
assert!(
AuthMechanism::Login.eq_mechanism(&AuthMechanism::Other("login".into())),
"Login must match Other(\"login\") (case-insensitive)"
);
assert!(
AuthMechanism::Login.eq_mechanism(&AuthMechanism::Other("Login".into())),
"Login must match Other(\"Login\") (mixed case)"
);
}
#[test]
fn supports_auth_cross_variant_other_login() {
let caps = ServerCapabilities {
greeting_name: "mail.example.com".into(),
extensions: vec![SmtpExtension::Auth(vec![AuthMechanism::Login])],
};
assert!(
caps.supports_auth(&AuthMechanism::Other("LOGIN".into())),
"Other(\"LOGIN\") query must match stored Login variant"
);
assert!(
caps.supports_auth(&AuthMechanism::Other("login".into())),
"Other(\"login\") query must match stored Login variant"
);
}
#[test]
fn eq_mechanism_login_does_not_match_plain() {
assert!(!AuthMechanism::Login.eq_mechanism(&AuthMechanism::Plain));
assert!(!AuthMechanism::Plain.eq_mechanism(&AuthMechanism::Login));
}
#[test]
fn is_data_ready_354() {
let resp = SmtpResponse {
code: 354,
enhanced_code: None,
lines: vec!["Start mail input".into()],
};
assert!(resp.is_data_ready());
}
#[test]
fn is_data_ready_rejects_other_3xx() {
let resp_355 = SmtpResponse {
code: 355,
enhanced_code: None,
lines: vec!["Not a real code".into()],
};
assert!(!resp_355.is_data_ready());
assert!(resp_355.is_intermediate(), "355 is still a 3xx code");
let resp_300 = SmtpResponse {
code: 300,
enhanced_code: None,
lines: vec!["Not a real code".into()],
};
assert!(!resp_300.is_data_ready());
}
#[test]
fn is_data_ready_rejects_non_3xx() {
for code in [250, 450, 550] {
let resp = SmtpResponse {
code,
enhanced_code: None,
lines: vec!["test".into()],
};
assert!(!resp.is_data_ready(), "code {code} must not be data-ready");
}
}