Index: smtp2tg.toml.example ================================================================== --- smtp2tg.toml.example +++ smtp2tg.toml.example @@ -6,30 +6,28 @@ api_gateway = "https://api.telegram.org" # <- note no trailing slash # where to listen on (sockets are not supported since 0.3.0) listen_on = "0.0.0.0:25" -# whether we need to handle unknown adresses -# - relay: send them to default one -# - deny: drop them -unknown = "relay" - -# default fields to show in message header -fields = [ "date", "from", "subject" ] +# default hostname to use when serving requests +hostname = "smtp2tg" # which domains are allowed in addresses # this means that any unqualified recipient "somebody" will also match # to "somebody@each_domain" domains = [ "localhost", "current.hostname" ] # default recipient, should be specified -# still can be a user, channel or group +# still can be a user, channel or group id default = 0 + +# default fields to show in message header +fields = [ "date", "from", "subject" ] [recipients] # make sure you quote emails, as "@" can't go there unquoted. And by default -# we need FQDNs +# we need FQDNs, also keep in mind emails are case insensitive "somebody@example.com" = 1 # user id's are positive "root" = -1 # group id's are negative # to look up chat/group id you can use debug settings in Telegram clients, # or some bot like @getidsbot or @RawDataBot Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -49,19 +49,22 @@ /// server fails to start. pub async fn async_main () -> Result<()> { let args = Args::parse(); let config_file = Path::new(&args.config); if !config_file.exists() { - bail!("can't read configuration from {config_file:?}"); + bail!("Configuration file not found: {config_file:?}\n\ + Hint: Ensure the file exists and the path is correct."); }; { let meta = metadata(config_file).await.stack()?; if (!0o100600 & meta.permissions().mode()) > 0 { - bail!("other users can read or write config file {config_file:?}\n\ - File permissions: {:o}", meta.permissions().mode()); - } - } + bail!("Configuration file permissions are insecure {config_file:?}\n\ + Current permissions: {:o}\n\ + Required: 0600 (owner read/write only).\n\ + Fix with: chmod 600 {config_file:?}", + meta.permissions().mode()); + } } 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()? @@ -68,12 +71,15 @@ .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"))?; + .with_context(|| format!( + "Failed to parse configuration file: {config_file:?}\n\ + Check syntax against smtp2tg.toml.example.\n\ + Common issues: missing quotes, trailing commas, or invalid types." + ))?; 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); Index: src/mail.rs ================================================================== --- src/mail.rs +++ src/mail.rs @@ -57,10 +57,11 @@ data: Vec, headers: Option, tg: Arc, fields: HashSet, address: Regex, + domains: HashSet, } impl MailServer { /// Initializes the mail server: sets up the Telegram API client and /// validates all required configuration values. @@ -93,27 +94,29 @@ 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}"); - } - } + bail!("Invalid domain in configuration: '{domain}'\n\ + Domains must be valid (e.g., 'example.com', 'localhost').\n\ + Check 'domains' array in smtp2tg.toml."); + } } if domains.is_empty() { bail!("No domains, need at least one: default `localhost` would do."); } - let domains = domains.into_iter().map(|s| escape(&s)) + let re_domains = domains.iter().map(|s| escape(s)) .collect::>().join("|"); - let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({domains}))?$")) + let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({re_domains}))?$")) .case_insensitive(true).build().stack()?; Ok(MailServer { data: vec!(), headers: None, tg, fields, address, + domains, }) } /// Retrieves the Telegram chat ID for a given email address, checks that /// used domain is allowed. @@ -126,13 +129,12 @@ /// not found. pub fn get_id (&self, name: &str) -> Result<&ChatPeerId> { if self.address.is_match(name) { Ok(self.tg.get(name).unwrap_or(&self.tg.default)) } else { - bail!("Doesn't look like address from one of our domains."); - } - } + bail!("Email address {name:?} is not from an allowed domain."); + } } /// Attempt to deliver one message async fn relay_mail (&self) -> Result<()> { if let Some(headers) = &self.headers { let mail = mail_parser::MessageParser::new().parse(&self.data) Index: src/telegram.rs ================================================================== --- src/telegram.rs +++ src/telegram.rs @@ -94,11 +94,11 @@ /// /// # Errors /// Returns an error if `name` is not configured. pub fn get (&self, name: &str) -> Result<&ChatPeerId> { self.recipients.get(&name.to_lowercase()) - .with_context(|| format!("Recipient \"{name}\" not found in configuration")) + .with_context(|| format!("Recipient {name:?} not found in configuration")) } /// Sends a text message to a specified chat. /// /// # Arguments Index: src/utils.rs ================================================================== --- src/utils.rs +++ src/utils.rs @@ -46,10 +46,9 @@ /// /// # Errors /// Returns an error if the text contains Telegram closing tags (``, ``). pub fn validate <'a>(text: &'a str) -> Result> { if RE_CLOSING.is_match(text) { - bail!("Telegram closing tag found."); + bail!("Text contains a Telegram closing tag (e.g., , )."); } else { Ok(encode_text(text)) - } -} +} }