Overview
Comment: | add possibility to specify which domains we are accepting |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk | v0.4.1 |
Files: | files | file ages | folders |
SHA3-256: |
2158b44929249d5c82060f3aa904139e |
User & Date: | arcade on 2025-06-12 13:19:29.182 |
Other Links: | manifest | tags |
Context
2025-06-21
| ||
08:45 | refactor, split to modules, change error handling, fix how default destination is specified Leaf check-in: f5ed284f8c user: arcade tags: trunk, v0.5.0 | |
2025-06-12
| ||
13:19 | add possibility to specify which domains we are accepting check-in: 2158b44929 user: arcade tags: trunk, v0.4.1 | |
2025-06-11
| ||
18:12 | switch to tgbot, change how files are sent check-in: d96b1b4710 user: arcade tags: trunk, v0.4.0 | |
Changes
Modified Cargo.lock
from [9bf27c3833]
to [9c6ae1199a].
︙ | ︙ | |||
699 700 701 702 703 704 705 706 707 708 709 710 711 712 | name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ | > > > > > > > > > > > | 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 | name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "hostname" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", "windows-link", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ |
︙ | ︙ | |||
1014 1015 1016 1017 1018 1019 1020 | [[package]] name = "libloading" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", | | | 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 | [[package]] name = "libloading" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", "windows-targets 0.53.1", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" |
︙ | ︙ | |||
1657 1658 1659 1660 1661 1662 1663 | name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smtp2tg" | | > | 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 | name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smtp2tg" version = "0.4.1" dependencies = [ "anyhow", "async-std", "config", "hostname", "just-getopt", "lazy_static", "mail-parser", "mailin-embedded", "regex", "tgbot", "thiserror", |
︙ | ︙ | |||
2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 | dependencies = [ "either", "home", "once_cell", "rustix 0.38.44", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", | > > > > > > | 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 | dependencies = [ "either", "home", "once_cell", "rustix 0.38.44", ] [[package]] name = "windows-link" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3bfe459f85da17560875b8bf1423d6f113b7a87a5d942e7da0ac71be7c61f8b" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", |
︙ | ︙ | |||
2182 2183 2184 2185 2186 2187 2188 | "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" | | | | 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 | "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30357ec391cde730f8fbfcdc29adc47518b06504528df977ab5af02ef23fdee9" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", |
︙ | ︙ |
Modified Cargo.toml
from [94d6ec3144]
to [40367f2f86].
1 2 | [package] name = "smtp2tg" | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [package] name = "smtp2tg" version = "0.4.1" authors = [ "arcade" ] edition = "2021" [dependencies] anyhow = "1.0.86" async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } config = { version = "0.15", default-features = false, features = [ "toml" ] } hostname = "0.4.1" just-getopt = "2.0.0" lazy_static = "1.5.0" regex = "1.11.1" tgbot = "0.36.1" thiserror = "2.0.11" mail-parser = { version = "0.11", features = ["serde"] } mailin-embedded = "^0" |
︙ | ︙ |
Modified smtp2tg.toml.example
from [ad2bc6b8b6]
to [b6e1bd141c].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" # 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" ] [recipients] # there should be default recipient, get's some debug info + mail that we # couldn't deliver (if enabled) _ = 1 # make sure you quote emails, as "@" can't go there unquoted. And by default # we need FQDNs "somebody@example.com" = 1 # user id's are positive "root@example.com" = -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 | > > > > > > > > > | 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 | # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" # 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" ] # which domains are allowed in addresses # this means that any unqualified recipient "somebody" will also match # to "somebody@each_domain" domains = [ "localhost", "current.hostname" ] [recipients] # there should be default recipient, get's some debug info + mail that we # couldn't deliver (if enabled) _ = 1 # make sure you quote emails, as "@" can't go there unquoted. And by default # we need FQDNs "somebody@example.com" = 1 # user id's are positive "root@example.com" = -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 |
Modified src/main.rs
from [8f6300706b]
to [08f17fe0a3].
︙ | ︙ | |||
14 15 16 17 18 19 20 | OptValue, }; use lazy_static::lazy_static; use mailin_embedded::{ Response, response::*, }; | | > > > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | OptValue, }; use lazy_static::lazy_static; use mailin_embedded::{ Response, response::*, }; use regex::{ Regex, escape, }; use tgbot::{ api::Client, types::{ ChatPeerId, InputFile, InputFileReader, InputMediaDocument, |
︙ | ︙ | |||
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | struct TelegramTransport { data: Vec<u8>, headers: Option<SomeHeaders>, recipients: HashMap<String, ChatPeerId>, relay: bool, tg: Client, fields: HashSet<String>, } lazy_static! { static ref RE_SPECIAL: Regex = Regex::new(r"([\-_*\[\]()~`>#+|{}\.!])").unwrap(); } /// Encodes special HTML entities to prevent them interfering with Telegram HTML fn encode (text: &str) -> Cow<'_, str> { RE_SPECIAL.replace_all(text, "\\$1") } | > > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | struct TelegramTransport { data: Vec<u8>, headers: Option<SomeHeaders>, recipients: HashMap<String, ChatPeerId>, relay: bool, tg: Client, fields: HashSet<String>, address: Regex, } lazy_static! { static ref RE_SPECIAL: Regex = Regex::new(r"([\-_*\[\]()~`>#+|{}\.!])").unwrap(); 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(); } /// Encodes special HTML entities to prevent them interfering with Telegram HTML fn encode (text: &str) -> Cow<'_, str> { RE_SPECIAL.replace_all(text, "\\$1") } |
︙ | ︙ | |||
129 130 131 132 133 134 135 136 137 138 139 140 141 142 | eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n"); panic!("no default recipient"); } let fields = HashSet::<String>::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 value = settings.get_string("unknown"); let relay = match value { Ok(value) => { match value.as_str() { "relay" => true, "deny" => false, _ => { eprintln!("[smtp2tg.toml] \"unknown\" should be either \"relay\" or \"deny\".\n"); | > > > > > > > > > > > > > | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n"); panic!("no default recipient"); } let fields = HashSet::<String>::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 value = settings.get_string("unknown"); let mut domains: HashSet<String> = HashSet::new(); let extra_domains = settings.get_array("domains").unwrap(); 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}"); } } let domains = domains.into_iter().map(|s| escape(&s)) .collect::<Vec<String>>().join("|"); let address = Regex::new(&format!("^(?P<user>[a-z0-9][-a-z0-9])(@({domains}))$")).unwrap(); let relay = match value { Ok(value) => { match value.as_str() { "relay" => true, "deny" => false, _ => { eprintln!("[smtp2tg.toml] \"unknown\" should be either \"relay\" or \"deny\".\n"); |
︙ | ︙ | |||
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | TelegramTransport { data: vec!(), headers: None, recipients, relay, tg, fields, } } /// Send message to default user, used for debug/log/info purposes async fn debug (&self, msg: &str) -> Result<Message, MyError> { self.send(self.recipients.get("_").ok_or(MyError::NoDefault)?, encode(msg)).await } /// Send message to specified user async fn send <S> (&self, to: &ChatPeerId, msg: S) -> Result<Message, MyError> where S: Into<String> { Ok(self.tg.execute( SendMessage::new(*to, msg) .with_parse_mode(MarkdownV2) ).await?) } /// Attempt to deliver one message async fn relay_mail (&self) -> Result<(), MyError> { if let Some(headers) = &self.headers { let mail = mail_parser::MessageParser::new().parse(&self.data) .ok_or(MyError::BadMail)?; // Adding all known addresses to recipient list, for anyone else adding default // Also if list is empty also adding default let mut rcpt: HashSet<&ChatPeerId> = HashSet::new(); if headers.to.is_empty() { return Err(MyError::NoRecipient); } for item in &headers.to { | > > > > > > > > > > > > > > > > > > > < < < < | < < < | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | TelegramTransport { data: vec!(), headers: None, recipients, relay, tg, fields, address, } } /// Send message to default user, used for debug/log/info purposes async fn debug (&self, msg: &str) -> Result<Message, MyError> { self.send(self.recipients.get("_").ok_or(MyError::NoDefault)?, encode(msg)).await } /// Send message to specified user async fn send <S> (&self, to: &ChatPeerId, msg: S) -> Result<Message, MyError> where S: Into<String> { Ok(self.tg.execute( SendMessage::new(*to, msg) .with_parse_mode(MarkdownV2) ).await?) } /// Returns id for provided email address fn get_id (&self, name: &str) -> Result<&ChatPeerId, MyError> { // here we need to store String locally to borrow it after let mut link = name; let name: String; if let Some(caps) = self.address.captures(link) { name = caps["name"].to_string(); link = &name; } match self.recipients.get(link) { Some(addr) => Ok(addr), None => { self.recipients.get("_") .ok_or(MyError::NoDefault) } } } /// Attempt to deliver one message async fn relay_mail (&self) -> Result<(), MyError> { if let Some(headers) = &self.headers { let mail = mail_parser::MessageParser::new().parse(&self.data) .ok_or(MyError::BadMail)?; // Adding all known addresses to recipient list, for anyone else adding default // Also if list is empty also adding default let mut rcpt: HashSet<&ChatPeerId> = HashSet::new(); if headers.to.is_empty() { return Err(MyError::NoRecipient); } for item in &headers.to { rcpt.insert(self.get_id(item)?); }; if rcpt.is_empty() { self.debug("No recipient or envelope address.").await?; rcpt.insert(self.recipients.get("_") .ok_or(MyError::NoDefault)?); }; |
︙ | ︙ | |||
369 370 371 372 373 374 375 | } /// Verify whether address is deliverable fn rcpt (&mut self, to: &str) -> Response { if self.relay { OK } else { | | | | | 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | } /// Verify whether address is deliverable fn rcpt (&mut self, to: &str) -> Response { if self.relay { OK } else { match self.get_id(to) { Ok(_) => OK, Err(_) => { if self.relay { OK } else { NO_MAILBOX } } } |
︙ | ︙ | |||
461 462 463 464 465 466 467 468 469 470 471 472 473 474 | } } let settings: config::Config = config::Config::builder() .set_default("fields", vec!["date", "from", "subject"]).unwrap() .set_default("hostname", "smtp.2.tg").unwrap() .set_default("listen_on", "0.0.0.0:1025").unwrap() .set_default("unknown", "relay").unwrap() .add_source(config::File::from(config_file)) .build() .unwrap_or_else(|_| panic!("[{config_file:?}] there was an error reading config\n\ \tplease consult \"smtp2tg.toml.example\" for details")); let listen_on = settings.get_string("listen_on")?; let server_name = settings.get_string("hostname")?; | > | 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 | } } let settings: config::Config = config::Config::builder() .set_default("fields", vec!["date", "from", "subject"]).unwrap() .set_default("hostname", "smtp.2.tg").unwrap() .set_default("listen_on", "0.0.0.0:1025").unwrap() .set_default("unknown", "relay").unwrap() .set_default("domains", vec!["localhost", hostname::get()?.to_str().expect("Failed to get current hostname")]).unwrap() .add_source(config::File::from(config_file)) .build() .unwrap_or_else(|_| panic!("[{config_file:?}] there was an error reading config\n\ \tplease consult \"smtp2tg.toml.example\" for details")); let listen_on = settings.get_string("listen_on")?; let server_name = settings.get_string("hostname")?; |
︙ | ︙ |