Overview
| Comment: | add proper tokio runtime, clean up junk about short headers, add api_gateway support |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | trunk | v0.5.3 |
| Files: | files | file ages | folders |
| SHA3-256: |
14ef340959fcb22426dc25979f8d6ee4 |
| User & Date: | arcade on 2026-01-01 08:47:31.916 |
| Other Links: | manifest | tags |
Context
|
2026-01-01
| ||
| 08:47 | add proper tokio runtime, clean up junk about short headers, add api_gateway support Leaf check-in: 14ef340959 user: arcade tags: trunk, v0.5.3 | |
| 07:15 | bump, switch to smol check-in: 072229b5bf user: arcade tags: trunk, v0.5.2 | |
Changes
Modified Cargo.lock
from [cc0ad4f102]
to [8f52ff6ce7].
| ︙ | ︙ | |||
18 19 20 21 22 23 24 25 26 27 28 29 30 31 | checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ | > > > > > > > > > > > > > | 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 | checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "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" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ |
| ︙ | ︙ | |||
1488 1489 1490 1491 1492 1493 1494 | "async-process", "blocking", "futures-lite", ] [[package]] name = "smtp2tg" | | > > | 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 | "async-process", "blocking", "futures-lite", ] [[package]] name = "smtp2tg" version = "0.5.3" dependencies = [ "async-compat", "config", "hostname", "just-getopt", "lazy_static", "mail-parser", "mailin-embedded", "regex", "smol", "stacked_errors", "tgbot", "tokio", ] [[package]] name = "socket2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" |
| ︙ | ︙ | |||
1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", "windows-sys 0.61.2", ] [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ | > > > > > > > > > > > > | 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "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" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ |
| ︙ | ︙ |
Modified Cargo.toml
from [540725f0b7]
to [b085dca268].
1 2 | [package] name = "smtp2tg" | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[package]
name = "smtp2tg"
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"] }
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
|
Modified smtp2tg.toml.example
from [f3d0bf9e93]
to [448e8e5d20].
1 2 3 4 5 6 7 8 9 10 11 | # vi:ft=toml: # 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" | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 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 # - deny: drop them unknown = "relay" |
| ︙ | ︙ |
Modified src/mail.rs
from [c0b6f52494]
to [7173095649].
| ︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
HashMap,
HashSet,
},
io::Error,
sync::Arc,
};
use mailin_embedded::{
Response,
response::{
INTERNAL_ERROR,
INVALID_CREDENTIALS,
NO_MAILBOX,
OK
| > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
HashMap,
HashSet,
},
io::Error,
sync::Arc,
};
use async_compat::Compat;
use mailin_embedded::{
Response,
response::{
INTERNAL_ERROR,
INVALID_CREDENTIALS,
NO_MAILBOX,
OK
|
| ︙ | ︙ | |||
67 68 69 70 71 72 73 |
for (name, value) in settings.get_table("recipients")
.expect("[smtp2tg.toml] missing table \"recipients\".\n")
{
let value = value.into_int()
.context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
recipients.insert(name, value);
}
| < < | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
for (name, value) in settings.get_table("recipients")
.expect("[smtp2tg.toml] missing table \"recipients\".\n")
{
let value = value.into_int()
.context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
recipients.insert(name, value);
}
let tg = Arc::new(TelegramTransport::new(api_key, recipients, &settings)?);
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 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();
|
| ︙ | ︙ | |||
151 152 153 154 155 156 157 |
if self.fields.contains("subject") {
if let Some(subject) = mail.subject() {
reply.push(format!("__*Subject:*__ `{}`", encode(subject)));
} else if let Some(thread) = mail.thread_name() {
reply.push(format!("__*Thread:*__ `{}`", encode(thread)));
}
}
| < | | < | 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
if self.fields.contains("subject") {
if let Some(subject) = mail.subject() {
reply.push(format!("__*Subject:*__ `{}`", encode(subject)));
} else if let Some(thread) = mail.thread_name() {
reply.push(format!("__*Thread:*__ `{}`", encode(thread)));
}
}
// do we need to replace spaces here?
if self.fields.contains("from") {
reply.push(format!("__*From:*__ `{}`", encode(&headers.from)));
}
if self.fields.contains("date") {
if let Some(date) = mail.date() {
reply.push(format!("__*Date:*__ `{date}`"));
}
}
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();
if html_parts != text_parts {
self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
|
| ︙ | ︙ | |||
308 309 310 311 312 313 314 |
self.data.append(buf.to_vec().as_mut());
Ok(())
}
/// Attempt to send email, return temporary error if that fails
fn data_end (&mut self) -> Response {
let mut result = OK;
| | | | 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
self.data.append(buf.to_vec().as_mut());
Ok(())
}
/// Attempt to send email, return temporary error if that fails
fn data_end (&mut self) -> Response {
let mut result = OK;
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
}
}
|
Modified src/main.rs
from [50e0c55c45]
to [e405a4dd11].
1 2 3 4 5 6 7 8 9 10 11 12 13 | //! 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; | | > > > | < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | | | | | | | | | | | | | | | | | | < | 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 |
//! 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 smol::{
fs::metadata,
};
use just_getopt::{
OptFlags,
OptSpecs,
OptValue,
};
use stacked_errors::{
Result,
StackableErr,
bail,
};
use std::{
io::Cursor,
os::unix::fs::PermissionsExt,
path::Path,
};
#[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(())
}
|
Modified src/telegram.rs
from [decd6d1c81]
to [dfced9c660].
| ︙ | ︙ | |||
40 41 42 43 44 45 46 |
tg: Client,
recipients: HashMap<String, ChatPeerId>,
pub default: ChatPeerId,
}
impl TelegramTransport {
| | > > > > | > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
tg: Client,
recipients: HashMap<String, ChatPeerId>,
pub default: ChatPeerId,
}
impl TelegramTransport {
pub fn new (api_key: String, recipients: HashMap<String, i64>, settings: &config::Config) -> Result<TelegramTransport> {
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")?
.with_host(api_gateway);
let recipients = recipients.into_iter()
.map(|(a, b)| (a, ChatPeerId::from(b))).collect();
let default = ChatPeerId::from(default);
Ok(TelegramTransport {
tg,
recipients,
|
| ︙ | ︙ |