ADDED src/lib.rs Index: src/lib.rs ================================================================== --- /dev/null +++ src/lib.rs @@ -0,0 +1,86 @@ +mod mail; +mod telegram; +pub mod utils; + +use crate::mail::MailServer; + +use just_getopt::{ + OptFlags, + OptSpecs, + OptValue, +}; +use smol::{ + fs::metadata, +}; +use stacked_errors::{ + Result, + StackableErr, + bail, +}; + +use std::{ + io::Cursor, + os::unix::fs::PermissionsExt, + path::Path, +}; + +/// Actual main function running async with Error propagation support +pub async fn async_main () -> Result<()> { + let specs = OptSpecs::new() + .option("help", "h", OptValue::None) + .option("help", "help", OptValue::None) + .option("config", "c", OptValue::Required) + .option("config", "config", OptValue::Required) + .flag(OptFlags::OptionsEverywhere); + let mut args = std::env::args(); + args.next(); + let parsed = specs.getopt(args); + for u in &parsed.unknown { + println!("Unknown option: {u}"); + } + if !(parsed.unknown.is_empty()) || parsed.options_first("help").is_some() { + println!("SMTP2TG v{}, (C) 2024 - 2026\n\n\ + \t-h|--help\tDisplay this help\n\ + \t-c|--config …\tSet configuration file location.", + env!("CARGO_PKG_VERSION")); + return Ok(()); + }; + let config_file = Path::new(if let Some(path) = parsed.options_value_last("config") { + &path[..] + } else { + "smtp2tg.toml" + }); + if !config_file.exists() { + bail!("can't read configuration from {config_file:?}"); + }; + { + 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()); + } + } + 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("unknown", "relay").stack()? + .set_default("domains", vec!["localhost", hostname::get().stack()?.to_str().expect("Failed to get current hostname")]).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); + + 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 @@ -106,13 +106,13 @@ address, }) } /// Returns id for provided email address - fn get_id (&self, name: &str) -> Result<&ChatPeerId> { + fn get_id (&self, name_str: &str) -> Result<&ChatPeerId> { // here we need to store String locally to borrow it after - let mut link = name; + let mut link = name_str; let name: String; if let Some(caps) = self.address.captures(link) { name = caps["name"].to_string(); link = &name; } Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -1,102 +1,15 @@ //! Simple SMTP-to-Telegram gateway. Can parse email and send them as telegram //! messages to specified chats, generally you specify which email address is //! available in configuration, everything else is sent to default address. -mod mail; -mod telegram; -mod utils; - -#[cfg(test)] -mod tests; - -use crate::mail::MailServer; - -use async_compat::Compat; -use just_getopt::{ - OptFlags, - OptSpecs, - OptValue, -}; -use smol::{ - fs::metadata, -}; -use stacked_errors::{ - Result, - StackableErr, - bail, -}; - -use std::{ - io::Cursor, - os::unix::fs::PermissionsExt, - path::Path, -}; - +use async_compat::Compat; +use stacked_errors:: Result; + +// main function stub that executes main code from lib fn main () -> Result<()> { smol::block_on(Compat::new(async { - async_main().await.unwrap() + smtp2tg::async_main().await.unwrap() })); - Ok(()) -} - -/// Actual main function running async with Error propagation support -async fn async_main () -> Result<()> { - let specs = OptSpecs::new() - .option("help", "h", OptValue::None) - .option("help", "help", OptValue::None) - .option("config", "c", OptValue::Required) - .option("config", "config", OptValue::Required) - .flag(OptFlags::OptionsEverywhere); - let mut args = std::env::args(); - args.next(); - let parsed = specs.getopt(args); - for u in &parsed.unknown { - println!("Unknown option: {u}"); - } - if !(parsed.unknown.is_empty()) || parsed.options_first("help").is_some() { - println!("SMTP2TG v{}, (C) 2024 - 2026\n\n\ - \t-h|--help\tDisplay this help\n\ - \t-c|--config …\tSet configuration file location.", - env!("CARGO_PKG_VERSION")); - return Ok(()); - }; - let config_file = Path::new(if let Some(path) = parsed.options_value_last("config") { - &path[..] - } else { - "smtp2tg.toml" - }); - if !config_file.exists() { - bail!("can't read configuration from {config_file:?}"); - }; - { - 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()); - } - } - 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("unknown", "relay").stack()? - .set_default("domains", vec!["localhost", hostname::get().stack()?.to_str().expect("Failed to get current hostname")]).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); - - server.with_name(server_name) - .with_ssl(mailin_embedded::SslConfig::None).unwrap() - .with_addr(listen_on).unwrap(); - server.serve().unwrap(); - Ok(()) } DELETED src/tests.rs Index: src/tests.rs ================================================================== --- src/tests.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::utils::validate; - -use stacked_errors::{ - Result, - StackableErr, -}; - -#[test] -fn check_valid () -> Result<()> { - let html = "

Some valid HTML

"; - let res = validate(html).stack()?; - assert_eq!(res, "<p>Some <b>valid</b> HTML</p>"); - Ok(()) -} - -#[test] -#[should_panic = "Telegram closing tag found."] -fn check_invalid () { - let html = "

Some valid HTML

Link injection!"; - let _ = validate(html).unwrap(); -} Index: src/utils.rs ================================================================== --- src/utils.rs +++ src/utils.rs @@ -9,24 +9,26 @@ bail, Result, }; lazy_static! { - pub static ref RE_DOMAIN: Regex = Regex::new(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$").unwrap(); - pub static ref RE_CLOSING: Regex = Regex::new(r"").unwrap(); + pub static ref RE_DOMAIN: Regex = Regex::new(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$").expect("Invalid domain regex"); + pub static ref RE_CLOSING: Regex = Regex::new(r"").expect("Invalid cloding tag regex"); } -/// `Attachment` object to store number attachment data and corresponding file name +/// Stores binary attachment data and metadata for Telegram messages. +/// The data is wrapped in a `Cursor>` for efficient streaming, +/// while `name` holds the filename or display name of the attachment. #[derive(Debug)] pub struct Attachment { pub data: Cursor>, pub name: String, } /// Pass any text here to be validated as not breaking from Telegram preformatted blocks /// escape all HTML chars afterwards -pub fn validate (text: &str) -> Result> { +pub fn validate <'a>(text: &'a str) -> Result> { if RE_CLOSING.is_match(text) { bail!("Telegram closing tag found."); } else { Ok(encode_text(text)) }