Annotation For tests/mail_relay.rs
Logged in as anonymous

Origin for each line in tests/mail_relay.rs from check-in e3cad77b2b:

e3cad77b2b 2026-10-28    1: //! Exercise the production mail handler with a recording transport. Including
e3cad77b2b 2026-10-28    2: //! the module here provides a test seam for its private Telegram dependency
e3cad77b2b 2026-10-28    3: //! without changing production visibility or making HTTP requests.
e3cad77b2b 2026-10-28    4: #[path = "../src/mail.rs"]
e3cad77b2b 2026-10-28    5: mod mail;
e3cad77b2b 2026-10-28    6: use smtp2tg::utils;
e3cad77b2b 2026-10-28    7: use std::io::Cursor;
e3cad77b2b 2026-10-28    8: use mailin_embedded::{Handler, response::{INTERNAL_ERROR, OK}};
e3cad77b2b 2026-10-28    9: use stacked_errors::{Result, StackableErr};
e3cad77b2b 2026-10-28   10: #[derive(Debug, PartialEq)]
e3cad77b2b 2026-10-28   11: enum Delivery {
e3cad77b2b 2026-10-28   12: 	Debug(String),
e3cad77b2b 2026-10-28   13: 	Text(String),
e3cad77b2b 2026-10-28   14: 	Files(Vec<(String, Vec<u8>)>, String),
e3cad77b2b 2026-10-28   15: }
e3cad77b2b 2026-10-28   16: mod telegram {
e3cad77b2b 2026-10-28   17: 	use super::{Delivery, utils::Attachment};
e3cad77b2b 2026-10-28   18: 	use std::{cell::RefCell, collections::HashMap};
e3cad77b2b 2026-10-28   19: 	use stacked_errors::{Result, StackableErr};
e3cad77b2b 2026-10-28   20: 	use tgbot::types::ChatPeerId;
e3cad77b2b 2026-10-28   21: 	thread_local! {
e3cad77b2b 2026-10-28   22: 		// Handler::data_end drives its future on the calling test thread.
e3cad77b2b 2026-10-28   23: 		static DELIVERIES: RefCell<Vec<Delivery>> = const { RefCell::new(Vec::new()) };
e3cad77b2b 2026-10-28   24: 	}
e3cad77b2b 2026-10-28   25: 	pub fn take_deliveries () -> Vec<Delivery> {
e3cad77b2b 2026-10-28   26: 		DELIVERIES.with(|items| std::mem::take(&mut *items.borrow_mut()))
e3cad77b2b 2026-10-28   27: 	}
e3cad77b2b 2026-10-28   28: 	#[derive(Debug)]
e3cad77b2b 2026-10-28   29: 	pub struct TelegramTransport {
e3cad77b2b 2026-10-28   30: 		pub default: ChatPeerId,
e3cad77b2b 2026-10-28   31: 		recipients: HashMap<String, ChatPeerId>,
e3cad77b2b 2026-10-28   32: 	}
e3cad77b2b 2026-10-28   33: 	impl TelegramTransport {
e3cad77b2b 2026-10-28   34: 		pub fn new (_api_key: String, recipients: HashMap<String, i64>, settings: &config::Config) -> Result<Self> {
e3cad77b2b 2026-10-28   35: 			Ok(Self {
e3cad77b2b 2026-10-28   36: 				default: settings.get_int("default").stack()?.into(),
e3cad77b2b 2026-10-28   37: 				recipients: recipients.into_iter().map(|(name, id)| (name, id.into())).collect(),
e3cad77b2b 2026-10-28   38: 			})
e3cad77b2b 2026-10-28   39: 		}
e3cad77b2b 2026-10-28   40: 		pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
e3cad77b2b 2026-10-28   41: 			self.recipients.get(&name.to_lowercase()).context("Unknown test recipient")
e3cad77b2b 2026-10-28   42: 		}
e3cad77b2b 2026-10-28   43: 		pub async fn debug (&self, msg: &str) -> Result<()> {
e3cad77b2b 2026-10-28   44: 			DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Debug(msg.into())));
e3cad77b2b 2026-10-28   45: 			Ok(())
e3cad77b2b 2026-10-28   46: 		}
e3cad77b2b 2026-10-28   47: 		pub async fn send (&self, _to: &ChatPeerId, msg: &str) -> Result<()> {
e3cad77b2b 2026-10-28   48: 			DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Text(msg.into())));
e3cad77b2b 2026-10-28   49: 			Ok(())
e3cad77b2b 2026-10-28   50: 		}
e3cad77b2b 2026-10-28   51: 		pub async fn sendgroup (&self, _to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> {
e3cad77b2b 2026-10-28   52: 			let files = media.into_iter().map(|file| (file.name, file.data.into_inner())).collect();
e3cad77b2b 2026-10-28   53: 			DELIVERIES.with(|items| items.borrow_mut().push(Delivery::Files(files, msg.into())));
e3cad77b2b 2026-10-28   54: 			Ok(())
e3cad77b2b 2026-10-28   55: 		}
e3cad77b2b 2026-10-28   56: 	}
e3cad77b2b 2026-10-28   57: }
e3cad77b2b 2026-10-28   58: fn server () -> Result<mail::MailServer> {
e3cad77b2b 2026-10-28   59: 	telegram::take_deliveries();
e3cad77b2b 2026-10-28   60: 	let settings = config::Config::builder()
e3cad77b2b 2026-10-28   61: 		.add_source(config::File::from_str(r#"
e3cad77b2b 2026-10-28   62: 			api_key = "test-api-key"
e3cad77b2b 2026-10-28   63: 			api_gateway = "https://api.telegram.org"
e3cad77b2b 2026-10-28   64: 			default = 0
e3cad77b2b 2026-10-28   65: 			fields = []
e3cad77b2b 2026-10-28   66: 			domains = ["example.com"]
e3cad77b2b 2026-10-28   67: 			[recipients]
e3cad77b2b 2026-10-28   68: 			"recipient@example.com" = 1
e3cad77b2b 2026-10-28   69: 		"#, config::FileFormat::Toml))
e3cad77b2b 2026-10-28   70: 		.build().stack()?;
e3cad77b2b 2026-10-28   71: 	mail::MailServer::new(settings)
e3cad77b2b 2026-10-28   72: }
e3cad77b2b 2026-10-28   73: fn relay (message: &str) -> Result<Vec<Delivery>> {
e3cad77b2b 2026-10-28   74: 	let mut server = server()?;
e3cad77b2b 2026-10-28   75: 	assert_eq!(server.data_start("example.com", "sender@example.com", true,
e3cad77b2b 2026-10-28   76: 		&["recipient@example.com".into()]), OK);
e3cad77b2b 2026-10-28   77: 	server.data(message.as_bytes()).stack()?;
e3cad77b2b 2026-10-28   78: 	assert_eq!(server.data_end(), OK);
e3cad77b2b 2026-10-28   79: 	Ok(telegram::take_deliveries())
e3cad77b2b 2026-10-28   80: }
e3cad77b2b 2026-10-28   81: fn caption (body: &str) -> String {
e3cad77b2b 2026-10-28   82: 	format!("<blockquote expandable>\n</blockquote><pre>{body}</pre>")
e3cad77b2b 2026-10-28   83: }
e3cad77b2b 2026-10-28   84: fn multipart (kind: &str, parts: &[&str]) -> String {
e3cad77b2b 2026-10-28   85: 	format!("MIME-Version: 1.0\r\nContent-Type: multipart/{kind}; boundary=parts\r\n\r\n--parts\r\n{}\r\n--parts--\r\n",
e3cad77b2b 2026-10-28   86: 		parts.join("\r\n--parts\r\n"))
e3cad77b2b 2026-10-28   87: }
e3cad77b2b 2026-10-28   88: const TEXT: &str = "Content-Type: text/plain; charset=utf-8\r\n\r\nPlain body";
e3cad77b2b 2026-10-28   89: const HTML: &str = "Content-Type: text/html; charset=utf-8\r\n\r\n<p>HTML body</p>";
e3cad77b2b 2026-10-28   90: #[test]
e3cad77b2b 2026-10-28   91: fn short_text_is_inlined_and_normalized () -> Result<()> {
e3cad77b2b 2026-10-28   92: 	let deliveries = relay("Content-Type: text/plain\r\n\r\nFirst & <second>\r\nThird")?;
e3cad77b2b 2026-10-28   93: 	assert_eq!(deliveries, vec![
e3cad77b2b 2026-10-28   94: 		Delivery::Text(caption("First &amp; &lt;second&gt;\nThird")),
e3cad77b2b 2026-10-28   95: 	]);
e3cad77b2b 2026-10-28   96: 	Ok(())
e3cad77b2b 2026-10-28   97: }
e3cad77b2b 2026-10-28   98: #[test]
e3cad77b2b 2026-10-28   99: fn alternative_html_is_not_sent_as_a_duplicate_attachment () -> Result<()> {
e3cad77b2b 2026-10-28  100: 	let deliveries = relay(&multipart("alternative", &[TEXT, HTML]))?;
e3cad77b2b 2026-10-28  101: 	assert_eq!(deliveries, vec![Delivery::Text(caption("Plain body"))]);
e3cad77b2b 2026-10-28  102: 	Ok(())
e3cad77b2b 2026-10-28  103: }
e3cad77b2b 2026-10-28  104: #[test]
e3cad77b2b 2026-10-28  105: fn html_only_message_uses_the_parsers_text_conversion () -> Result<()> {
e3cad77b2b 2026-10-28  106: 	assert_eq!(relay(HTML)?, vec![
e3cad77b2b 2026-10-28  107: 		Delivery::Text(caption("HTML body\n")),
e3cad77b2b 2026-10-28  108: 	]);
e3cad77b2b 2026-10-28  109: 	Ok(())
e3cad77b2b 2026-10-28  110: }
e3cad77b2b 2026-10-28  111: #[test]
e3cad77b2b 2026-10-28  112: fn remaining_text_parts_are_sent_once_in_order () -> Result<()> {
e3cad77b2b 2026-10-28  113: 	let second = "Content-Type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nSecond=20body";
e3cad77b2b 2026-10-28  114: 	let third = "Content-Type: text/plain\r\n\r\nThird body";
e3cad77b2b 2026-10-28  115: 	assert_eq!(relay(&multipart("mixed", &[TEXT, second, third]))?, vec![
e3cad77b2b 2026-10-28  116: 		Delivery::Files(vec![
e3cad77b2b 2026-10-28  117: 			("Attachment.txt".into(), b"Second body".to_vec()),
e3cad77b2b 2026-10-28  118: 			("Attachment.txt".into(), b"Third body".to_vec()),
e3cad77b2b 2026-10-28  119: 		], caption("Plain body")),
e3cad77b2b 2026-10-28  120: 	]);
e3cad77b2b 2026-10-28  121: 	Ok(())
e3cad77b2b 2026-10-28  122: }
e3cad77b2b 2026-10-28  123: #[test]
e3cad77b2b 2026-10-28  124: fn text_at_the_message_limit_is_sent_as_a_decoded_attachment () -> Result<()> {
e3cad77b2b 2026-10-28  125: 	// The existing contract requires the complete escaped message to be <4096
e3cad77b2b 2026-10-28  126: 	// bytes. Exercise both sides so the first text iterator item cannot be lost.
e3cad77b2b 2026-10-28  127: 	let capacity = 4096 - caption("").len();
e3cad77b2b 2026-10-28  128: 	for size in [capacity - 1, capacity] {
e3cad77b2b 2026-10-28  129: 		let body = "x".repeat(size);
e3cad77b2b 2026-10-28  130: 		let message = format!("Content-Type: text/plain\r\n\r\n{body}");
e3cad77b2b 2026-10-28  131: 		let deliveries = relay(&message)?;
e3cad77b2b 2026-10-28  132: 		assert_eq!(deliveries.len(), 1);
e3cad77b2b 2026-10-28  133: 		let expected = if size < capacity {
e3cad77b2b 2026-10-28  134: 			Delivery::Text(caption(&body))
e3cad77b2b 2026-10-28  135: 		} else {
e3cad77b2b 2026-10-28  136: 			Delivery::Files(vec![("Attachment.txt".into(), body.into_bytes())], caption(""))
e3cad77b2b 2026-10-28  137: 		};
e3cad77b2b 2026-10-28  138: 		assert_eq!(deliveries[0], expected, "body size: {size}");
e3cad77b2b 2026-10-28  139: 	}
e3cad77b2b 2026-10-28  140: 	Ok(())
e3cad77b2b 2026-10-28  141: }
e3cad77b2b 2026-10-28  142: #[test]
e3cad77b2b 2026-10-28  143: fn attachment_payloads_are_transfer_decoded_and_keep_content_type_names () -> Result<()> {
e3cad77b2b 2026-10-28  144: 	let binary = "Content-Type: application/octet-stream; name=\"payload.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w==";
e3cad77b2b 2026-10-28  145: 	let quoted = "Content-Type: application/octet-stream; name=\"notes.txt\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\ncaf=C3=A9=0Asecond=3Dline";
e3cad77b2b 2026-10-28  146: 	assert_eq!(relay(&multipart("mixed", &[TEXT, binary, quoted]))?, vec![
e3cad77b2b 2026-10-28  147: 		Delivery::Files(vec![
e3cad77b2b 2026-10-28  148: 			("payload.bin".into(), vec![0, 1, 2, 255]),
e3cad77b2b 2026-10-28  149: 			("notes.txt".into(), "café\nsecond=line".as_bytes().to_vec()),
e3cad77b2b 2026-10-28  150: 		], caption("Plain body")),
e3cad77b2b 2026-10-28  151: 	]);
e3cad77b2b 2026-10-28  152: 	Ok(())
e3cad77b2b 2026-10-28  153: }
e3cad77b2b 2026-10-28  154: #[test]
e3cad77b2b 2026-10-28  155: fn unnamed_attachment_uses_the_fallback_filename () -> Result<()> {
e3cad77b2b 2026-10-28  156: 	let attachment = "Content-Type: application/octet-stream\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\naGVsbG8=";
e3cad77b2b 2026-10-28  157: 	assert_eq!(relay(&multipart("mixed", &[attachment]))?, vec![
e3cad77b2b 2026-10-28  158: 		Delivery::Files(vec![("Attachment.txt".into(), b"hello".to_vec())], caption("")),
e3cad77b2b 2026-10-28  159: 	]);
e3cad77b2b 2026-10-28  160: 	Ok(())
e3cad77b2b 2026-10-28  161: }
e3cad77b2b 2026-10-28  162: #[test]
e3cad77b2b 2026-10-28  163: fn attachment_filename_is_preserved_for_case_insensitive_mime_headers () -> Result<()> {
e3cad77b2b 2026-10-28  164: 	// MIME field names are case insensitive. The old parser's Header::name()
e3cad77b2b 2026-10-28  165: 	// returned canonical names; raw_name() now preserves their original case.
e3cad77b2b 2026-10-28  166: 	for header in ["Content-Type", "content-type", "CONTENT-TYPE", "cOnTeNt-TyPe"] {
e3cad77b2b 2026-10-28  167: 		let attachment = format!("{header}: application/octet-stream; name=\"report.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w==");
e3cad77b2b 2026-10-28  168: 		assert_eq!(relay(&multipart("mixed", &[&attachment]))?, vec![
e3cad77b2b 2026-10-28  169: 			Delivery::Files(vec![("report.bin".into(), vec![0, 1, 2, 255])], caption("")),
e3cad77b2b 2026-10-28  170: 		], "MIME header: {header}");
e3cad77b2b 2026-10-28  171: 	}
e3cad77b2b 2026-10-28  172: 	Ok(())
e3cad77b2b 2026-10-28  173: }
e3cad77b2b 2026-10-28  174: #[test]
e3cad77b2b 2026-10-28  175: fn nested_alternatives_and_files_use_separate_iterators () -> Result<()> {
e3cad77b2b 2026-10-28  176: 	let alternative = multipart("alternative", &[TEXT, HTML]).replace("parts", "inner");
e3cad77b2b 2026-10-28  177: 	let extra = "Content-Type: text/plain\r\n\r\nExtra body";
e3cad77b2b 2026-10-28  178: 	let attachment = "Content-Type: application/octet-stream; name=\"file.bin\"\r\nContent-Disposition: attachment\r\nContent-Transfer-Encoding: base64\r\n\r\nAAEC/w==";
e3cad77b2b 2026-10-28  179: 	assert_eq!(relay(&multipart("mixed", &[&alternative, extra, attachment]))?, vec![
e3cad77b2b 2026-10-28  180: 		Delivery::Files(vec![
e3cad77b2b 2026-10-28  181: 			("Attachment.txt".into(), b"Extra body".to_vec()),
e3cad77b2b 2026-10-28  182: 			("file.bin".into(), vec![0, 1, 2, 255]),
e3cad77b2b 2026-10-28  183: 		], caption("Plain body")),
e3cad77b2b 2026-10-28  184: 	]);
e3cad77b2b 2026-10-28  185: 	Ok(())
e3cad77b2b 2026-10-28  186: }
e3cad77b2b 2026-10-28  187: #[test]
e3cad77b2b 2026-10-28  188: fn empty_text_body_does_not_create_a_spurious_attachment () -> Result<()> {
e3cad77b2b 2026-10-28  189: 	assert_eq!(relay("Content-Type: text/plain\r\n\r\n")?, vec![
e3cad77b2b 2026-10-28  190: 		Delivery::Text(caption("")),
e3cad77b2b 2026-10-28  191: 	]);
e3cad77b2b 2026-10-28  192: 	Ok(())
e3cad77b2b 2026-10-28  193: }
e3cad77b2b 2026-10-28  194: #[test]
e3cad77b2b 2026-10-28  195: fn forbidden_closing_tag_stops_delivery () -> Result<()> {
e3cad77b2b 2026-10-28  196: 	let mut server = server()?;
e3cad77b2b 2026-10-28  197: 	server.data_start("example.com", "sender@example.com", false,
e3cad77b2b 2026-10-28  198: 		&["recipient@example.com".into()]);
e3cad77b2b 2026-10-28  199: 	server.data(b"Content-Type: text/plain\r\n\r\nUnsafe </pre> body").stack()?;
e3cad77b2b 2026-10-28  200: 	assert_eq!(server.data_end(), INTERNAL_ERROR);
e3cad77b2b 2026-10-28  201: 	let deliveries = telegram::take_deliveries();
e3cad77b2b 2026-10-28  202: 	assert_eq!(deliveries.len(), 1);
e3cad77b2b 2026-10-28  203: 	assert!(matches!(&deliveries[0], Delivery::Debug(message)
e3cad77b2b 2026-10-28  204: 		if message.starts_with("Sending emails failed:")));
e3cad77b2b 2026-10-28  205: 	Ok(())
e3cad77b2b 2026-10-28  206: }