Index: Cargo.lock ================================================================== --- Cargo.lock +++ Cargo.lock @@ -20,10 +20,23 @@ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] [[package]] name = "async-executor" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1490,12 +1503,13 @@ "futures-lite", ] [[package]] name = "smtp2tg" -version = "0.5.2" +version = "0.5.3" dependencies = [ + "async-compat", "config", "hostname", "just-getopt", "lazy_static", "mail-parser", @@ -1502,10 +1516,11 @@ "mailin-embedded", "regex", "smol", "stacked_errors", "tgbot", + "tokio", ] [[package]] name = "socket2" version = "0.6.1" @@ -1665,12 +1680,24 @@ "bytes", "libc", "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,12 +1,13 @@ [package] name = "smtp2tg" -version = "0.5.2" +version = "0.5.3" authors = [ "arcade" ] edition = "2021" [dependencies] +async-compat = "0.2.5" config = { version = "0.15", default-features = false, features = [ "toml" ] } hostname = "0.4.1" just-getopt = "2.0.0" lazy_static = "1.5.0" mail-parser = { version = "0.11", features = ["serde"] } @@ -13,9 +14,10 @@ mailin-embedded = "^0" regex = "1.11.1" smol = "2.0.2" stacked_errors = "0.7.1" tgbot = "0.40" +tokio = { version = "~1", features = [ "macros", "rt" ] } [profile.release] lto = true codegen-units = 1 Index: smtp2tg.toml.example ================================================================== --- smtp2tg.toml.example +++ smtp2tg.toml.example @@ -1,9 +1,12 @@ # vi:ft=toml: # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" +# Telegram API gateway (when you are running your own) +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 Index: src/mail.rs ================================================================== --- src/mail.rs +++ src/mail.rs @@ -18,10 +18,11 @@ }, io::Error, sync::Arc, }; +use async_compat::Compat; use mailin_embedded::{ Response, response::{ INTERNAL_ERROR, INVALID_CREDENTIALS, @@ -69,14 +70,12 @@ { let value = value.into_int() .context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?; recipients.insert(name, value); } - let default = settings.get_int("default") - .context("[smtp2tg.toml] missing \"default\" recipient.\n")?; - let tg = Arc::new(TelegramTransport::new(api_key, recipients, default)?); + 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"))); let mut domains: HashSet = HashSet::new(); let extra_domains = settings.get_array("domains").stack()?; @@ -153,21 +152,19 @@ reply.push(format!("__*Subject:*__ `{}`", encode(subject))); } else if let Some(thread) = mail.thread_name() { reply.push(format!("__*Thread:*__ `{}`", encode(thread))); } } - let mut short_headers: Vec = vec![]; // do we need to replace spaces here? if self.fields.contains("from") { - short_headers.push(format!("__*From:*__ `{}`", encode(&headers.from))); + reply.push(format!("__*From:*__ `{}`", encode(&headers.from))); } if self.fields.contains("date") { if let Some(date) = mail.date() { - short_headers.push(format!("__*Date:*__ `{date}`")); + reply.push(format!("__*Date:*__ `{date}`")); } } - reply.push(short_headers.join(" ")); let header_size = reply.join(" ").len() + 1; let html_parts = mail.html_body_count(); let text_parts = mail.text_body_count(); let attachments = mail.attachment_count(); @@ -310,22 +307,22 @@ } /// Attempt to send email, return temporary error if that fails fn data_end (&mut self) -> Response { let mut result = OK; - smol::block_on(async { + smol::block_on(Compat::new(async { // relay mail if let Err(err) = self.relay_mail().await { result = INTERNAL_ERROR; // in case that fails - inform default recipient if let Err(err) = self.tg.debug(&format!("Sending emails failed:\n{err:?}")).await { // in case that also fails - write some logs and bail eprintln!("{err:?}"); }; }; - }); + })); // clear - just in case self.data = vec![]; self.headers = None; result } } Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -9,11 +9,13 @@ #[cfg(test)] mod tests; use crate::mail::MailServer; -use smol::fs::metadata; +use smol::{ + fs::metadata, +}; use just_getopt::{ OptFlags, OptSpecs, OptValue, }; @@ -27,65 +29,65 @@ io::Cursor, os::unix::fs::PermissionsExt, path::Path, }; -fn main () -> Result<()> { - smol::block_on(async { - 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 - 2025\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("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(()) - }) +#[tokio::main(flavor = "current_thread")] +async fn 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 - 2025\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/telegram.rs ================================================================== --- src/telegram.rs +++ src/telegram.rs @@ -42,13 +42,18 @@ pub default: ChatPeerId, } impl TelegramTransport { - pub fn new (api_key: String, recipients: HashMap, default: i64) -> Result { + pub fn new (api_key: String, recipients: HashMap, settings: &config::Config) -> Result { + let default = settings.get_int("default") + .context("[smtp2tg.toml] missing \"default\" recipient.\n")?; + let api_gateway = settings.get_string("api_gateway") + .context("[smtp2tg.toml] missing \"api_gateway\" destination.\n")?; let tg = Client::new(api_key) - .context("Failed to create API.\n")?; + .context("Failed to create API.\n")? + .with_host(api_gateway); let recipients = recipients.into_iter() .map(|(a, b)| (a, ChatPeerId::from(b))).collect(); let default = ChatPeerId::from(default); Ok(TelegramTransport {