Check-in [14ef340959]
Logged in as anonymous
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: 14ef340959fcb22426dc25979f8d6ee41e6e13ca0fdb5856a54fee61772a0612
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
18
19
20
21
22
23
24













25
26
27
28
29
30
31
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
1495

1496

1497
1498
1499
1500
1501
1502
1503
1504
1505
1506

1507
1508
1509
1510
1511
1512
1513
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.2"
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
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 = [
1
2
3

4
5
6
7

8
9
10
11
12
13
14
15
16
17

18
19
20
21
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.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"] }
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
1
2
3
4



5
6
7
8
9
10
11
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"
16
17
18
19
20
21
22

23
24
25
26
27
28
29
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
74
75
76
77

78
79
80
81
82
83
84
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 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::<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
158
159
160
161

162
163
164
165

166
167
168
169
170
171
172
173
174
175
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)));
				}
			}
			let mut short_headers: Vec<String> = 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();
			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
315

316
317
318
319
320
321
322
323
324
325

326
327
328
329
330
331
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(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
	}
}
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
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 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")]
fn main () -> Result<()> {
async 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 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);
	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();
	server.with_name(server_name)
		.with_ssl(mailin_embedded::SslConfig::None).unwrap()
		.with_addr(listen_on).unwrap();
	server.serve().unwrap();

		Ok(())
	Ok(())
	})
}
40
41
42
43
44
45
46
47





48
49


50
51
52
53
54
55
56
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>, default: i64) -> Result<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")?;
			.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,