use bytes::BytesMut;
use crate::types::{BodyType, DeliverByMode, DsnNotify, DsnRet, MailFromParams, RcptToParams};
fn encode_greeting(buf: &mut BytesMut, cmd: &[u8], domain: &str) {
buf.extend_from_slice(cmd);
buf.extend_from_slice(domain.as_bytes());
buf.extend_from_slice(b"\r\n");
}
pub(crate) fn encode_ehlo(buf: &mut BytesMut, domain: &str) {
encode_greeting(buf, b"EHLO ", domain);
}
#[cfg(test)]
pub(crate) fn encode_auth_plain(buf: &mut BytesMut, user: &str, pass: &str) {
use base64::Engine;
let mut credentials = Vec::with_capacity(1 + user.len() + 1 + pass.len());
credentials.push(0);
credentials.extend_from_slice(user.as_bytes());
credentials.push(0);
credentials.extend_from_slice(pass.as_bytes());
let encoded = base64::engine::general_purpose::STANDARD.encode(&credentials);
buf.extend_from_slice(b"AUTH PLAIN ");
buf.extend_from_slice(encoded.as_bytes());
buf.extend_from_slice(b"\r\n");
}
#[cfg(test)]
pub(crate) fn encode_auth_xoauth2(buf: &mut BytesMut, user: &str, token: &str) {
use base64::Engine;
let sasl_string = format!("user={user}\x01auth=Bearer {token}\x01\x01");
let encoded = base64::engine::general_purpose::STANDARD.encode(sasl_string.as_bytes());
buf.extend_from_slice(b"AUTH XOAUTH2 ");
buf.extend_from_slice(encoded.as_bytes());
buf.extend_from_slice(b"\r\n");
}
#[cfg(test)]
pub(crate) fn encode_auth_oauthbearer(buf: &mut BytesMut, token: &str) {
use base64::Engine;
let sasl_string = format!("n,,\x01auth=Bearer {token}\x01\x01");
let encoded = base64::engine::general_purpose::STANDARD.encode(sasl_string.as_bytes());
buf.extend_from_slice(b"AUTH OAUTHBEARER ");
buf.extend_from_slice(encoded.as_bytes());
buf.extend_from_slice(b"\r\n");
}
#[cfg(test)]
pub(crate) fn encode_auth_login_initial(buf: &mut BytesMut) {
buf.extend_from_slice(b"AUTH LOGIN\r\n");
}
pub(crate) fn encode_mail_from(buf: &mut BytesMut, from: &str, size: Option<u64>) {
let params = MailFromParams {
size,
..MailFromParams::default()
};
encode_mail_from_full(buf, from, ¶ms);
}
pub(crate) fn encode_rcpt_to(buf: &mut BytesMut, to: &str) {
buf.extend_from_slice(b"RCPT TO:<");
buf.extend_from_slice(to.as_bytes());
buf.extend_from_slice(b">\r\n");
}
pub(crate) fn encode_rcpt_to_full(buf: &mut BytesMut, to: &str, params: &RcptToParams) {
buf.extend_from_slice(b"RCPT TO:<");
buf.extend_from_slice(to.as_bytes());
buf.extend_from_slice(b">");
if let Some(notify) = ¶ms.notify {
buf.extend_from_slice(b" NOTIFY=");
if notify.iter().any(|n| matches!(n, DsnNotify::Never)) {
buf.extend_from_slice(b"NEVER");
} else {
let mut first = true;
for n in notify {
if !first {
buf.extend_from_slice(b",");
}
first = false;
match n {
DsnNotify::Success => buf.extend_from_slice(b"SUCCESS"),
DsnNotify::Failure => buf.extend_from_slice(b"FAILURE"),
DsnNotify::Delay => buf.extend_from_slice(b"DELAY"),
DsnNotify::Never => unreachable!(),
}
}
}
}
if let Some(orcpt) = ¶ms.orcpt {
buf.extend_from_slice(b" ORCPT=rfc822;");
encode_xtext(buf, orcpt);
}
buf.extend_from_slice(b"\r\n");
}
pub(crate) fn encode_data(buf: &mut BytesMut) {
buf.extend_from_slice(b"DATA\r\n");
}
pub(crate) fn encode_data_end(buf: &mut BytesMut, preceding_data: &[u8]) {
if !preceding_data.ends_with(b"\r\n") {
buf.extend_from_slice(b"\r\n");
}
buf.extend_from_slice(b".\r\n");
}
pub(crate) fn encode_starttls(buf: &mut BytesMut) {
buf.extend_from_slice(b"STARTTLS\r\n");
}
pub(crate) fn encode_quit(buf: &mut BytesMut) {
buf.extend_from_slice(b"QUIT\r\n");
}
pub(crate) fn encode_rset(buf: &mut BytesMut) {
buf.extend_from_slice(b"RSET\r\n");
}
pub(crate) fn encode_noop(buf: &mut BytesMut) {
buf.extend_from_slice(b"NOOP\r\n");
}
pub(crate) fn encode_bdat(buf: &mut BytesMut, size: usize, last: bool) {
buf.extend_from_slice(b"BDAT ");
buf.extend_from_slice(size.to_string().as_bytes());
if last {
buf.extend_from_slice(b" LAST");
}
buf.extend_from_slice(b"\r\n");
}
pub(crate) fn encode_lhlo(buf: &mut BytesMut, domain: &str) {
encode_greeting(buf, b"LHLO ", domain);
}
pub(crate) fn encode_helo(buf: &mut BytesMut, domain: &str) {
encode_greeting(buf, b"HELO ", domain);
}
pub(crate) fn encode_mail_from_full(buf: &mut BytesMut, from: &str, params: &MailFromParams) {
buf.extend_from_slice(b"MAIL FROM:<");
buf.extend_from_slice(from.as_bytes());
buf.extend_from_slice(b">");
if let Some(size) = params.size {
buf.extend_from_slice(b" SIZE=");
buf.extend_from_slice(size.to_string().as_bytes());
}
if let Some(body) = ¶ms.body {
match body {
BodyType::SevenBit => buf.extend_from_slice(b" BODY=7BIT"),
BodyType::EightBitMime => buf.extend_from_slice(b" BODY=8BITMIME"),
BodyType::BinaryMime => buf.extend_from_slice(b" BODY=BINARYMIME"),
}
}
if params.smtputf8 {
buf.extend_from_slice(b" SMTPUTF8");
}
if params.requiretls {
buf.extend_from_slice(b" REQUIRETLS");
}
if let Some(ret) = ¶ms.ret {
match ret {
DsnRet::Full => buf.extend_from_slice(b" RET=FULL"),
DsnRet::Hdrs => buf.extend_from_slice(b" RET=HDRS"),
}
}
if let Some(envid) = ¶ms.envid {
buf.extend_from_slice(b" ENVID=");
encode_xtext(buf, envid);
}
if let Some(hold_for) = params.hold_for {
buf.extend_from_slice(b" HOLDFOR=");
buf.extend_from_slice(hold_for.to_string().as_bytes());
}
if let Some(hold_until) = ¶ms.hold_until {
buf.extend_from_slice(b" HOLDUNTIL=");
buf.extend_from_slice(hold_until.as_bytes());
}
if let Some(deliver_by) = ¶ms.deliver_by {
buf.extend_from_slice(b" BY=");
buf.extend_from_slice(deliver_by.seconds.to_string().as_bytes());
match deliver_by.mode {
DeliverByMode::Notify => buf.extend_from_slice(b";N"),
DeliverByMode::Return => buf.extend_from_slice(b";R"),
}
}
if let Some(mt_priority) = params.mt_priority {
buf.extend_from_slice(b" MT-PRIORITY=");
buf.extend_from_slice(mt_priority.to_string().as_bytes());
}
buf.extend_from_slice(b"\r\n");
}
fn encode_xtext(buf: &mut BytesMut, s: &str) {
for &b in s.as_bytes() {
if b == b'+' || b <= 0x20 || b > 0x7E {
buf.extend_from_slice(b"+");
buf.extend_from_slice(format!("{b:02X}").as_bytes());
} else {
buf.extend_from_slice(&[b]);
}
}
}
fn encode_cmd_with_arg(buf: &mut BytesMut, cmd: &[u8], arg: &str) {
buf.extend_from_slice(cmd);
buf.extend_from_slice(arg.as_bytes());
buf.extend_from_slice(b"\r\n");
}
pub(crate) fn encode_vrfy(buf: &mut BytesMut, address: &str) {
encode_cmd_with_arg(buf, b"VRFY ", address);
}
pub(crate) fn encode_expn(buf: &mut BytesMut, list_name: &str) {
encode_cmd_with_arg(buf, b"EXPN ", list_name);
}
pub(crate) fn dot_stuff(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len() + data.len() / 50);
let mut at_line_start = true;
let mut prev_cr = false;
for &byte in data {
if at_line_start && byte == b'.' {
result.push(b'.');
}
result.push(byte);
at_line_start = byte == b'\n' && prev_cr;
prev_cr = byte == b'\r';
}
result
}
#[cfg(test)]
pub(crate) fn dot_stuff_size(data: &[u8]) -> usize {
let mut size = data.len();
let mut at_line_start = true;
let mut prev_cr = false;
for &byte in data {
if at_line_start && byte == b'.' {
size += 1;
}
at_line_start = byte == b'\n' && prev_cr;
prev_cr = byte == b'\r';
}
size
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn encode_ehlo_command() {
let mut buf = BytesMut::new();
encode_ehlo(&mut buf, "client.example.com");
assert_eq!(&buf[..], b"EHLO client.example.com\r\n");
}
#[test]
fn encode_mail_from_without_size() {
let mut buf = BytesMut::new();
encode_mail_from(&mut buf, "sender@example.com", None);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com>\r\n");
}
#[test]
fn encode_mail_from_with_size() {
let mut buf = BytesMut::new();
encode_mail_from(&mut buf, "sender@example.com", Some(1024));
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> SIZE=1024\r\n");
}
#[test]
fn encode_rcpt_to_command() {
let mut buf = BytesMut::new();
encode_rcpt_to(&mut buf, "recipient@example.com");
assert_eq!(&buf[..], b"RCPT TO:<recipient@example.com>\r\n");
}
#[test]
fn dot_stuffing_no_dots() {
assert_eq!(dot_stuff(b"hello\r\nworld\r\n"), b"hello\r\nworld\r\n");
}
#[test]
fn dot_stuffing_leading_dot() {
assert_eq!(
dot_stuff(b".hidden\r\n.also\r\n"),
b"..hidden\r\n..also\r\n"
);
}
#[test]
fn dot_stuffing_dot_in_middle() {
assert_eq!(dot_stuff(b"no.dot\r\n"), b"no.dot\r\n");
}
#[test]
fn dot_stuffing_at_start_of_data() {
assert_eq!(dot_stuff(b".start"), b"..start");
}
#[test]
fn dot_stuffing_bare_lf_not_treated_as_line_start() {
assert_eq!(
dot_stuff(b"test\n.notastart\r\n"),
b"test\n.notastart\r\n",
"dot after bare LF must not be stuffed (RFC 5321 Section 4.5.2)"
);
}
#[test]
fn dot_stuffing_crlf_dot_is_stuffed() {
assert_eq!(
dot_stuff(b"test\r\n.start\r\n"),
b"test\r\n..start\r\n",
"dot after CRLF must be stuffed (RFC 5321 Section 4.5.2)"
);
}
#[test]
fn dot_stuff_size_matches_dot_stuff_len() {
let cases: &[&[u8]] = &[
b"hello\r\nworld\r\n",
b".hidden\r\n.also\r\n",
b"no.dot\r\n",
b".start",
b"test\n.notastart\r\n",
b"test\r\n.start\r\n",
b"",
b"Subject: Test\r\n\r\n.line1\r\n.line2\r\n",
];
for data in cases {
assert_eq!(
dot_stuff_size(data),
dot_stuff(data).len(),
"dot_stuff_size mismatch for {:?}",
String::from_utf8_lossy(data)
);
}
}
#[test]
fn auth_plain_encoding() {
use base64::Engine;
let mut buf = BytesMut::new();
encode_auth_plain(&mut buf, "user", "pass");
let line = std::str::from_utf8(&buf).unwrap();
assert!(line.starts_with("AUTH PLAIN "));
assert!(line.ends_with("\r\n"));
let b64 = &line["AUTH PLAIN ".len()..line.len() - 2];
let decoded = base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap();
assert_eq!(decoded, b"\0user\0pass");
}
#[test]
fn auth_xoauth2_encoding() {
use base64::Engine;
let mut buf = BytesMut::new();
encode_auth_xoauth2(&mut buf, "user@example.com", "ya29.token");
let line = std::str::from_utf8(&buf).unwrap();
assert!(line.starts_with("AUTH XOAUTH2 "));
let b64 = &line["AUTH XOAUTH2 ".len()..line.len() - 2];
let decoded = base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap();
let expected = "user=user@example.com\x01auth=Bearer ya29.token\x01\x01";
assert_eq!(decoded, expected.as_bytes());
}
#[test]
fn auth_plain_exact_base64_matches_manual_computation() {
use base64::Engine;
let user = "testuser";
let pass = "testpass";
let mut credentials = Vec::with_capacity(1 + user.len() + 1 + pass.len());
credentials.push(0);
credentials.extend_from_slice(user.as_bytes());
credentials.push(0);
credentials.extend_from_slice(pass.as_bytes());
let expected_b64 = base64::engine::general_purpose::STANDARD.encode(&credentials);
let mut buf = BytesMut::new();
encode_auth_plain(&mut buf, user, pass);
let line = std::str::from_utf8(&buf).unwrap();
let actual_b64 = &line["AUTH PLAIN ".len()..line.len() - 2];
assert_eq!(
actual_b64, expected_b64,
"encode_auth_plain base64 must match manual RFC 4616 computation"
);
}
#[test]
fn auth_xoauth2_exact_base64_matches_manual_computation() {
use base64::Engine;
let user = "user@example.com";
let token = "ya29.a0token";
let sasl_string = format!("user={user}\x01auth=Bearer {token}\x01\x01");
let expected_b64 = base64::engine::general_purpose::STANDARD.encode(sasl_string.as_bytes());
let mut buf = BytesMut::new();
encode_auth_xoauth2(&mut buf, user, token);
let line = std::str::from_utf8(&buf).unwrap();
let actual_b64 = &line["AUTH XOAUTH2 ".len()..line.len() - 2];
assert_eq!(
actual_b64, expected_b64,
"encode_auth_xoauth2 base64 must match manual XOAUTH2 SASL computation"
);
}
#[test]
fn encode_bdat_without_last() {
let mut buf = BytesMut::new();
encode_bdat(&mut buf, 1024, false);
assert_eq!(&buf[..], b"BDAT 1024\r\n");
}
#[test]
fn encode_bdat_with_last() {
let mut buf = BytesMut::new();
encode_bdat(&mut buf, 512, true);
assert_eq!(&buf[..], b"BDAT 512 LAST\r\n");
}
#[test]
fn encode_lhlo_command() {
let mut buf = BytesMut::new();
encode_lhlo(&mut buf, "client.example.com");
assert_eq!(&buf[..], b"LHLO client.example.com\r\n");
}
#[test]
fn encode_helo_command() {
let mut buf = BytesMut::new();
encode_helo(&mut buf, "client.example.com");
assert_eq!(&buf[..], b"HELO client.example.com\r\n");
}
#[test]
fn encode_mail_from_full_no_params() {
let mut buf = BytesMut::new();
encode_mail_from_full(&mut buf, "sender@example.com", &MailFromParams::default());
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com>\r\n");
}
#[test]
fn encode_mail_from_full_with_size() {
let mut buf = BytesMut::new();
let params = MailFromParams {
size: Some(2048),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> SIZE=2048\r\n");
}
#[test]
fn encode_mail_from_full_with_body_8bitmime() {
let mut buf = BytesMut::new();
let params = MailFromParams {
body: Some(BodyType::EightBitMime),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> BODY=8BITMIME\r\n"
);
}
#[test]
fn encode_mail_from_full_with_body_binarymime() {
let mut buf = BytesMut::new();
let params = MailFromParams {
body: Some(BodyType::BinaryMime),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> BODY=BINARYMIME\r\n"
);
}
#[test]
fn encode_mail_from_full_with_body_7bit() {
let mut buf = BytesMut::new();
let params = MailFromParams {
body: Some(BodyType::SevenBit),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> BODY=7BIT\r\n");
}
#[test]
fn encode_mail_from_full_with_smtputf8() {
let mut buf = BytesMut::new();
let params = MailFromParams {
smtputf8: true,
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> SMTPUTF8\r\n");
}
#[test]
fn encode_mail_from_full_all_params() {
let mut buf = BytesMut::new();
let params = MailFromParams {
size: Some(4096),
body: Some(BodyType::EightBitMime),
smtputf8: true,
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> SIZE=4096 BODY=8BITMIME SMTPUTF8\r\n"
);
}
#[test]
fn data_end_no_extra_blank_line_when_data_ends_with_crlf() {
let data = b"Subject: Test\r\n\r\nHello\r\n";
let stuffed = dot_stuff(data);
let mut terminator = BytesMut::new();
encode_data_end(&mut terminator, &stuffed);
let mut wire = Vec::new();
wire.extend_from_slice(&stuffed);
wire.extend_from_slice(&terminator);
assert!(
wire.ends_with(b"Hello\r\n.\r\n"),
"expected wire to end with 'Hello\\r\\n.\\r\\n', got trailing bytes: {:?}",
String::from_utf8_lossy(&wire[wire.len().saturating_sub(20)..])
);
}
#[test]
fn data_end_adds_crlf_when_data_does_not_end_with_crlf() {
let data = b"incomplete line";
let mut buf = BytesMut::new();
encode_data_end(&mut buf, data);
assert_eq!(&buf[..], b"\r\n.\r\n");
}
#[test]
fn data_end_with_empty_data() {
let mut buf = BytesMut::new();
encode_data_end(&mut buf, b"");
assert_eq!(&buf[..], b"\r\n.\r\n");
}
#[test]
fn encode_quit_command() {
let mut buf = BytesMut::new();
encode_quit(&mut buf);
assert_eq!(&buf[..], b"QUIT\r\n");
}
#[test]
fn encode_vrfy_command() {
let mut buf = BytesMut::new();
encode_vrfy(&mut buf, "user@example.com");
assert_eq!(&buf[..], b"VRFY user@example.com\r\n");
}
#[test]
fn encode_expn_command() {
let mut buf = BytesMut::new();
encode_expn(&mut buf, "staff");
assert_eq!(&buf[..], b"EXPN staff\r\n");
}
#[test]
fn encode_mail_from_equiv_no_params() {
let mut a = BytesMut::new();
let mut b = BytesMut::new();
encode_mail_from(&mut a, "test@example.com", None);
encode_mail_from_full(&mut b, "test@example.com", &MailFromParams::default());
assert_eq!(&a[..], &b[..]);
}
#[test]
fn encode_mail_from_equiv_with_size() {
let mut a = BytesMut::new();
let mut b = BytesMut::new();
encode_mail_from(&mut a, "test@example.com", Some(5000));
let params = MailFromParams {
size: Some(5000),
..Default::default()
};
encode_mail_from_full(&mut b, "test@example.com", ¶ms);
assert_eq!(&a[..], &b[..]);
}
#[test]
fn auth_login_initial_encoding() {
let mut buf = BytesMut::new();
encode_auth_login_initial(&mut buf);
assert_eq!(&buf[..], b"AUTH LOGIN\r\n");
}
#[test]
fn encode_rcpt_to_full_empty_params() {
let mut a = BytesMut::new();
let mut b = BytesMut::new();
encode_rcpt_to(&mut a, "user@example.com");
encode_rcpt_to_full(&mut b, "user@example.com", &RcptToParams::default());
assert_eq!(&a[..], &b[..]);
}
#[test]
fn encode_mail_from_full_with_ret_full() {
let mut buf = BytesMut::new();
let params = MailFromParams {
ret: Some(DsnRet::Full),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> RET=FULL\r\n");
}
#[test]
fn encode_mail_from_full_with_ret_hdrs() {
let mut buf = BytesMut::new();
let params = MailFromParams {
ret: Some(DsnRet::Hdrs),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> RET=HDRS\r\n");
}
#[test]
fn encode_mail_from_full_with_envid() {
let mut buf = BytesMut::new();
let params = MailFromParams {
envid: Some("msg-12345".into()),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> ENVID=msg-12345\r\n"
);
}
#[test]
fn encode_mail_from_full_envid_xtext_encoding() {
let mut buf = BytesMut::new();
let params = MailFromParams {
envid: Some("id with+plus".into()),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> ENVID=id+20with+2Bplus\r\n"
);
}
#[test]
fn encode_mail_from_full_with_ret_and_envid() {
let mut buf = BytesMut::new();
let params = MailFromParams {
ret: Some(DsnRet::Hdrs),
envid: Some("envelope-42".into()),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> RET=HDRS ENVID=envelope-42\r\n"
);
}
#[test]
fn encode_rcpt_to_full_with_notify_success() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![DsnNotify::Success]),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(&buf[..], b"RCPT TO:<user@example.com> NOTIFY=SUCCESS\r\n");
}
#[test]
fn encode_rcpt_to_full_with_notify_multiple() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![
DsnNotify::Success,
DsnNotify::Failure,
DsnNotify::Delay,
]),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(
&buf[..],
b"RCPT TO:<user@example.com> NOTIFY=SUCCESS,FAILURE,DELAY\r\n"
);
}
#[test]
fn encode_rcpt_to_full_with_notify_never() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![DsnNotify::Never]),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(&buf[..], b"RCPT TO:<user@example.com> NOTIFY=NEVER\r\n");
}
#[test]
fn encode_rcpt_to_full_with_orcpt() {
let mut buf = BytesMut::new();
let params = RcptToParams {
orcpt: Some("user@example.com".into()),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(
&buf[..],
b"RCPT TO:<user@example.com> ORCPT=rfc822;user@example.com\r\n"
);
}
#[test]
fn encode_rcpt_to_full_with_notify_and_orcpt() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![DsnNotify::Success, DsnNotify::Failure]),
orcpt: Some("original@example.com".into()),
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(
&buf[..],
b"RCPT TO:<user@example.com> NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;original@example.com\r\n"
);
}
#[test]
fn rcpt_to_params_is_empty() {
assert!(RcptToParams::default().is_empty());
assert!(!RcptToParams {
notify: Some(vec![DsnNotify::Success]),
..Default::default()
}
.is_empty());
assert!(!RcptToParams {
orcpt: Some("user@example.com".into()),
..Default::default()
}
.is_empty());
}
#[test]
fn xtext_encoding_printable_ascii_passthrough() {
let mut buf = BytesMut::new();
encode_xtext(&mut buf, "user@example.com");
assert_eq!(&buf[..], b"user@example.com");
}
#[test]
fn xtext_encoding_plus_is_encoded() {
let mut buf = BytesMut::new();
encode_xtext(&mut buf, "a+b");
assert_eq!(&buf[..], b"a+2Bb");
}
#[test]
fn xtext_encoding_space_is_encoded() {
let mut buf = BytesMut::new();
encode_xtext(&mut buf, "a b");
assert_eq!(&buf[..], b"a+20b");
}
#[test]
fn encode_mail_from_full_with_requiretls() {
let mut buf = BytesMut::new();
let params = MailFromParams {
requiretls: true,
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> REQUIRETLS\r\n");
}
#[test]
fn encode_mail_from_full_requiretls_false_omitted() {
let mut buf = BytesMut::new();
let params = MailFromParams {
requiretls: false,
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com>\r\n");
}
#[test]
fn auth_oauthbearer_encoding() {
use base64::Engine;
let mut buf = BytesMut::new();
encode_auth_oauthbearer(&mut buf, "ya29.token");
let line = std::str::from_utf8(&buf).unwrap();
assert!(line.starts_with("AUTH OAUTHBEARER "));
assert!(line.ends_with("\r\n"));
let b64 = &line["AUTH OAUTHBEARER ".len()..line.len() - 2];
let decoded = base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap();
let expected = "n,,\x01auth=Bearer ya29.token\x01\x01";
assert_eq!(decoded, expected.as_bytes());
}
#[test]
fn encode_mail_from_full_with_holdfor() {
let mut buf = BytesMut::new();
let params = MailFromParams {
hold_for: Some(86400),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> HOLDFOR=86400\r\n"
);
}
#[test]
fn encode_mail_from_full_with_holduntil() {
let mut buf = BytesMut::new();
let params = MailFromParams {
hold_until: Some("2024-12-25T00:00:00Z".into()),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> HOLDUNTIL=2024-12-25T00:00:00Z\r\n"
);
}
#[test]
fn encode_mail_from_full_with_deliver_by_return() {
use crate::types::{DeliverBy, DeliverByMode};
let mut buf = BytesMut::new();
let params = MailFromParams {
deliver_by: Some(DeliverBy {
seconds: 3600,
mode: DeliverByMode::Return,
}),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> BY=3600;R\r\n");
}
#[test]
fn encode_mail_from_full_with_deliver_by_notify() {
use crate::types::{DeliverBy, DeliverByMode};
let mut buf = BytesMut::new();
let params = MailFromParams {
deliver_by: Some(DeliverBy {
seconds: -120,
mode: DeliverByMode::Notify,
}),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(&buf[..], b"MAIL FROM:<sender@example.com> BY=-120;N\r\n");
}
#[test]
fn encode_mail_from_full_with_mt_priority() {
let mut buf = BytesMut::new();
let params = MailFromParams {
mt_priority: Some(3),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> MT-PRIORITY=3\r\n"
);
}
#[test]
fn encode_mail_from_full_with_mt_priority_negative() {
let mut buf = BytesMut::new();
let params = MailFromParams {
mt_priority: Some(-4),
..Default::default()
};
encode_mail_from_full(&mut buf, "sender@example.com", ¶ms);
assert_eq!(
&buf[..],
b"MAIL FROM:<sender@example.com> MT-PRIORITY=-4\r\n"
);
}
#[test]
fn encode_rcpt_to_full_notify_never_not_combined_with_others() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![DsnNotify::Never, DsnNotify::Success]),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(
&buf[..],
b"RCPT TO:<user@example.com> NOTIFY=NEVER\r\n",
"NEVER must not be combined with other NOTIFY values (RFC 3461 Section 4.1)"
);
}
#[test]
fn encode_rcpt_to_full_notify_never_alone_unchanged() {
let mut buf = BytesMut::new();
let params = RcptToParams {
notify: Some(vec![DsnNotify::Never]),
..Default::default()
};
encode_rcpt_to_full(&mut buf, "user@example.com", ¶ms);
assert_eq!(&buf[..], b"RCPT TO:<user@example.com> NOTIFY=NEVER\r\n");
}
}