Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -64,12 +64,12 @@ .set_default("api_gateway", "https://api.telegram.org").stack()? .set_default("fields", vec!["date", "from", "subject"]).stack()? .set_default("hostname", "smtp.2.tg").stack()? .set_default("listen_on", "0.0.0.0:1025").stack()? .set_default("domains", vec!["localhost", - hostname::get().expect("Failed to get current hostname") - .to_str().expect("Can't convert hostname to string, bad UTF-8?")]).stack()? + hostname::get().context("Failed to get current hostname")? + .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()? .add_source(config::File::from(config_file)) .build() .with_context(|| format!("[{config_file:?}] there was an error reading config\n\ \tplease consult \"smtp2tg.toml.example\" for details"))?; @@ -76,12 +76,13 @@ let listen_on = settings.get_string("listen_on").stack()?; let server_name = settings.get_string("hostname").stack()?; let core = MailServer::new(settings)?; let mut server = mailin_embedded::Server::new(core); + // TODO: remove unwraps when mailin-embedded bumps with better error handling server.with_name(server_name) .with_ssl(mailin_embedded::SslConfig::None).unwrap() .with_addr(listen_on).unwrap(); server.serve().unwrap(); Ok(()) } Index: src/mail.rs ================================================================== --- src/mail.rs +++ src/mail.rs @@ -74,31 +74,35 @@ pub fn new (settings: config::Config) -> Result { let api_key = settings.get_string("api_key") .context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?; let mut recipients = HashMap::new(); for (name, value) in settings.get_table("recipients") - .expect("[smtp2tg.toml] missing table \"recipients\".\n") + .context("[smtp2tg.toml] missing table \"recipients\".\n")? { let value = value.into_int() .context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?; recipients.insert(name.to_lowercase().replace('.', ""), value); } let tg = Arc::new(TelegramTransport::new(api_key, recipients, &settings)?); let fields = HashSet::::from_iter(settings.get_array("fields") - .expect("[smtp2tg.toml] \"fields\" should be an array") - .iter().map(|x| x.clone().into_string().expect("should be strings"))); + .context("[smtp2tg.toml] \"fields\" should be an array")? + .iter().map(|x| x.clone().into_string().context("should be strings")) + .collect::>>()?); let mut domains: HashSet = HashSet::new(); let extra_domains = settings.get_array("domains").stack()?; for domain in extra_domains { let domain = domain.to_string().to_lowercase(); if RE_DOMAIN.is_match(&domain) { domains.insert(domain); } else { - panic!("[smtp2tg.toml] can't check of domains in \"domains\": {domain}"); + bail!("[smtp2tg.toml] can't check domains in \"domains\": {domain}"); } } + if domains.is_empty() { + bail!("No domains, need at least one: default `localhost` would do."); + } let domains = domains.into_iter().map(|s| escape(&s)) .collect::>().join("|"); let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({domains}))?$")) .case_insensitive(true).build().stack()?; Index: tests/mail.rs ================================================================== --- tests/mail.rs +++ tests/mail.rs @@ -1,17 +1,20 @@ use smtp2tg::mail::MailServer; +use config::FileFormat::Toml; use stacked_errors::{ Result, StackableErr, + ensure, + ensure_eq, }; use tgbot::types::ChatPeerId; -/// Builds a `MailServer` purely from an in-memory TOML source, no -/// network access is performed while constructing it. -fn build_server () -> Result { - let settings = config::Config::builder() + +#[test] +fn get_id_properly_resolves_addresses () -> Result<()> { + let server = MailServer::new(config::Config::builder() .add_source(config::File::from_str(r#" api_key = "test-api-key" api_gateway = "https://api.telegram.org" default = 0 fields = ["date", "from", "subject"] @@ -18,19 +21,13 @@ domains = ["example.com"] [recipients] "someone@example.com" = 1 "root" = -1 - "#, config::FileFormat::Toml)) + "#, Toml)) .build() - .stack()?; - MailServer::new(settings) -} - -#[test] -fn get_id_properly_resolves_addresses () -> Result<()> { - let server = build_server()?; + .stack()?)?; let cases = [ ("someone@example.com", 1), ("someone", 0), ("root", -1), ("unknown@example.com", 0), @@ -38,21 +35,65 @@ ("someone@EXAMPLE.COM", 1), // uppercase domain ("some.one@example.com", 1), // functionally equivalent to skipping '.' ("some-one-2", 0), // Hyphens ]; for (email, id) in cases { - assert_eq!(*server.get_id(email)?, ChatPeerId::from(id), "email [{email}] expected to return id [{id}]"); + ensure_eq!(*server.get_id(email)?, ChatPeerId::from(id), format!("email [{email}] expected to return id [{id}]")); } let cases = [ "someone@otherdomain.net", "@example.com", // empty local part "some@one@example.com", // more than one '@' "someone@example.com.evil", "someone@example.org", ]; for email in cases { - let err = server.get_id(email).err() - .ok_or_else(|| format!("email [{email}] expected to fail")).stack()?; - assert!(err.to_string().contains("Doesn't look like address from one of our domains.")); + ensure!(server.get_id(email).is_err(), format!("this email should be rejected: {email}")); + } + Ok(()) +} + +#[test] +fn wrong_server_config () -> Result<()> { + let configs = [ + "domains = []", + "", + "[recipents]\na = 1", + r#" + api_key = "test-api-key" + api_gateway = "https://api.telegram.org" + default = 0 + fields = ["date", "from", "subject"] + domains = ["example.com"] + # no recipients + "#, + r#" + ap_key = "test-api-key" # bad one + api_gateway = "https://api.telegram.org" + default = 0 + fields = ["date", "from", "subject"] + domains = ["example.com"] + + [recipients] + "someone@example.com" = 1 + "root" = -1"#, + r#" + api_key = "test-api-key" + api_gateway = "https://api.telegram.org" + default = 0 + fields = ["date", "from", "subject"] + domains = [] # empty + + [recipients] + "someone@example.com" = 1 + "root" = -1"#, + ]; + for config in configs { + let settings = config::Config::builder() + .add_source(config::File::from_str(config, Toml)) + .build() + .stack()?; + ensure!(MailServer::new(settings).is_err(), format!("this config shouldn't be valid:\n{config}")); } + Ok(()) } Index: tests/utils.rs ================================================================== --- tests/utils.rs +++ tests/utils.rs @@ -7,11 +7,14 @@ use std::{ borrow::Cow, mem::discriminant, }; -use stacked_errors::Result; +use stacked_errors::{ + Result, + ensure_eq, +}; #[test] fn test_validate_escaping_behavior () -> Result<()> { let cases: &[(&str, Cow)] = &[ // `validate` escapes HTML special characters. @@ -27,18 +30,18 @@ ("plain text without special html characters", Cow::Borrowed("plain text without special html characters")), ("5 > 3 & 2 < 4", Cow::Owned("5 > 3 & 2 < 4".into())), ]; for (input, expected) in cases { let result = validate(input)?; - assert_eq!(&result, expected, "unexpected output for input {input:?}"); - assert_eq!(discriminant(&result), discriminant(expected), "wrong Cow variant for input {input:?}"); + ensure_eq!(&result, expected, format!("unexpected output for input {input:?}")); + ensure_eq!(discriminant(&result), discriminant(expected), format!("wrong Cow variant for input {input:?}")); } Ok(()) } #[test] -fn test_validate_closing_tag_behavior () { +fn test_validate_closing_tag_behavior () -> Result<()> { let cases = [ ("", true), ("", true), ("", false), ("", true), @@ -51,16 +54,17 @@ ("\n", true), ("

Some valid HTML

Link injection!", true), ("
", false),
 	];
 	for (input, expected) in cases {
-		assert_eq!(RE_CLOSING.is_match(input), expected, "unexpected match result for {input:?}");
+		ensure_eq!(RE_CLOSING.is_match(input), expected, format!("unexpected match result for {input:?}"));
 	}
+	Ok(())
 }
 
 #[test]
-fn test_regex_domain_behavior() {
+fn test_regex_domain_behavior() -> Result<()> {
 	let cases = [
 		("", false),
 		("-example.com", false),
 		(".example.com", false),
 		("123.456", true),
@@ -74,8 +78,9 @@
 		("invalid@domain.com", false),
 		("my-host.example.com", true),
 		("sub.example.co.uk", true),
 	];
 	for (input, expected) in cases {
-		assert_eq!(RE_DOMAIN.is_match(input), expected, "unexpected match result for {input:?}");
+		ensure_eq!(RE_DOMAIN.is_match(input), expected, format!("unexpected match result for {input:?}"));
 	}
+	Ok(())
 }