Overview
| Comment: | ensure!() we don't panic but instead return Result whenever possible, convert error handling a little, add TODO for unwrap(), more sanity checks, simplify and expand testing |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
98c5a42df066f1639688919741b16b89 |
| User & Date: | arcade on 2026-08-01 18:47:58.519 |
| Other Links: | manifest | tags |
Context
|
2026-08-01
| ||
| 19:29 | drop dot removal (wrong), document lowercasing check-in: aaa78fed23 user: arcade tags: trunk | |
| 18:47 | ensure!() we don't panic but instead return Result whenever possible, convert error handling a little, add TODO for unwrap(), more sanity checks, simplify and expand testing check-in: 98c5a42df0 user: arcade tags: trunk | |
| 15:30 | fix regexp, get rid of relaying variants as they are actually noop after move to mailin check-in: 158c9cffc6 user: arcade tags: trunk | |
Changes
Modified src/lib.rs
from [3665254a0d]
to [984fc307f8].
| ︙ | ︙ | |||
62 63 64 65 66 67 68 |
}
let settings: config::Config = config::Config::builder()
.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",
| | | > | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
}
let settings: config::Config = config::Config::builder()
.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().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"))?;
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(())
}
|
Modified src/mail.rs
from [eb64cf1123]
to [2ddc87b771].
| ︙ | ︙ | |||
72 73 74 75 76 77 78 |
/// Returns an error if required configuration values are missing or invalid.
/// server fails to start.
pub fn new (settings: config::Config) -> Result<MailServer> {
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")
| | | | > | > > > | 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
/// Returns an error if required configuration values are missing or invalid.
/// server fails to start.
pub fn new (settings: config::Config) -> Result<MailServer> {
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")
.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::<String>::from_iter(settings.get_array("fields")
.context("[smtp2tg.toml] \"fields\" should be an array")?
.iter().map(|x| x.clone().into_string().context("should be strings"))
.collect::<Result<Vec<String>>>()?);
let mut domains: HashSet<String> = 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 {
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::<Vec<String>>().join("|");
let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({domains}))?$"))
.case_insensitive(true).build().stack()?;
Ok(MailServer {
data: vec!(),
|
| ︙ | ︙ |
Modified tests/mail.rs
from [5b6e412716]
to [695401d00e].
1 2 3 4 5 6 7 8 |
use smtp2tg::mail::MailServer;
use stacked_errors::{
Result,
StackableErr,
};
use tgbot::types::ChatPeerId;
| > > > | | | | | | < < < < < < | > > > > | > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > | > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
use smtp2tg::mail::MailServer;
use config::FileFormat::Toml;
use stacked_errors::{
Result,
StackableErr,
ensure,
ensure_eq,
};
use tgbot::types::ChatPeerId;
#[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"]
domains = ["example.com"]
[recipients]
"someone@example.com" = 1
"root" = -1
"#, Toml))
.build()
.stack()?)?;
let cases = [
("someone@example.com", 1),
("someone", 0),
("root", -1),
("unknown@example.com", 0),
("SOMEONE@example.com", 1), // uppercase local part
("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 {
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 {
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(())
}
|
Modified tests/utils.rs
from [afc49aa801]
to [4c9edd9d43].
1 2 3 4 5 6 7 8 9 10 11 |
use smtp2tg::utils::{
validate,
RE_CLOSING,
RE_DOMAIN,
};
use std::{
borrow::Cow,
mem::discriminant,
};
| | > > > | | | | > | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
use smtp2tg::utils::{
validate,
RE_CLOSING,
RE_DOMAIN,
};
use std::{
borrow::Cow,
mem::discriminant,
};
use stacked_errors::{
Result,
ensure_eq,
};
#[test]
fn test_validate_escaping_behavior () -> Result<()> {
let cases: &[(&str, Cow<str>)] = &[
// `validate` escapes HTML special characters.
("<p>Some <b>valid</b> HTML</p>", Cow::Owned("<p>Some <b>valid</b> HTML</p>".into())),
// Empty input is returned unchanged.
("", Cow::Borrowed("")),
// Whitespace-only input needs no escaping.
(" \t\n", Cow::Borrowed(" \t\n")),
// `validate` returns `Cow<'a, str>` borrowed from its input lifetime `'a`.
// These two cases exercise both branches of that `Cow` to make sure the
// explicit lifetime introduced on `validate` still lets callers observe a
// zero-copy borrow when no escaping is required.
("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)?;
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 () -> Result<()> {
let cases = [
("</ pre >", true),
("</\tcode\t>", true),
("</b>", false),
("</Code>", true),
("</code>", true),
("</code>\t", true),
("</code>\t>", true),
("</div>", false), // Not a pre/code tag
("</PRE>", true),
("</pre>", true),
("</pre>\n", true),
("<p>Some <b>valid</b> HTML</p></code><a href='http://somewere.com'>Link injection!</a>", true),
("<pre>", false),
];
for (input, expected) in cases {
ensure_eq!(RE_CLOSING.is_match(input), expected, format!("unexpected match result for {input:?}"));
}
Ok(())
}
#[test]
fn test_regex_domain_behavior() -> Result<()> {
let cases = [
("", false),
("-example.com", false),
(".example.com", false),
("123.456", true),
("EXAMPLE.COM", false),
("a", true),
("a.b", true),
("example-.com", false),
("example..com", false),
("example.com", true),
("example.com.", false),
("invalid@domain.com", false),
("my-host.example.com", true),
("sub.example.co.uk", true),
];
for (input, expected) in cases {
ensure_eq!(RE_DOMAIN.is_match(input), expected, format!("unexpected match result for {input:?}"));
}
Ok(())
}
|