Check-in [512369f93e]
Logged in as anonymous
Overview
Comment:bump, add some docstrings, switch from just-getopt to clap
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 512369f93ea42dd7527ba7a79b352617ae6274e26d029f430a918a229d98720c
User & Date: arcade on 2026-08-01 11:20:12.682
Other Links: manifest | tags
Context
2026-08-01
11:30
remove serde dep for mail-parser check-in: b9bf773618 user: arcade tags: trunk
11:20
bump, add some docstrings, switch from just-getopt to clap check-in: 512369f93e user: arcade tags: trunk
10:46
expand README check-in: 880ce86a7f user: arcade tags: trunk
Changes
1
2
3
4
5
6
7
8
9
10
11
12


















































13
14
15
16
17
18
19
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
 "memchr",
]



















































[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [












>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
 "memchr",
]

[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "is_terminal_polyfill",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"

[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
 "windows-sys 0.61.2",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
 "anstyle",
 "once_cell_polyfill",
 "windows-sys 0.61.2",
]

[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
289
290
291
292
293
294
295








































296
297
298
299
300
301
302
303
304






305
306
307
308
309
310
311
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
 "cfg-if",
 "cpufeatures",
 "rand_core",
]









































[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
 "cc",
]







[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
 "bytes",







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>









>
>
>
>
>
>







339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
 "cfg-if",
 "cpufeatures",
 "rand_core",
]

[[package]]
name = "clap"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
]

[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn 3.0.3",
]

[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
 "cc",
]

[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"

[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
 "bytes",
619
620
621
622
623
624
625






626
627
628
629
630
631
632
dependencies = [
 "indexmap",
 "proc-macro2",
 "quote",
 "syn 2.0.119",
]







[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"

[[package]]







>
>
>
>
>
>







715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
dependencies = [
 "indexmap",
 "proc-macro2",
 "quote",
 "syn 2.0.119",
]

[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"

[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"

[[package]]
864
865
866
867
868
869
870






871
872
873
874
875
876
877

[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"







[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"

[[package]]







>
>
>
>
>
>







966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985

[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"

[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"

[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"

[[package]]
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
 "cfg-if",
 "futures-util",
 "wasm-bindgen",
]

[[package]]
name = "just-getopt"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97185bbe8f4edbaf6a3acc4f3f723358488d2da28e8f12a83529ddc6268f9c30"

[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"

[[package]]







<
<
<
<
<
<







1048
1049
1050
1051
1052
1053
1054






1055
1056
1057
1058
1059
1060
1061
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
 "cfg-if",
 "futures-util",
 "wasm-bindgen",
]







[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"

[[package]]
1075
1076
1077
1078
1079
1080
1081






1082
1083
1084
1085
1086
1087
1088

[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"







[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"

[[package]]







>
>
>
>
>
>







1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196

[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"

[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"

[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"

[[package]]
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676

1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
 "async-process",
 "blocking",
 "futures-lite",
]

[[package]]
name = "smtp2tg"
version = "0.6.6"
dependencies = [
 "async-compat",

 "config",
 "hostname",
 "html-escape",
 "just-getopt",
 "lazy_static",
 "mail-parser",
 "mailin-embedded",
 "regex",
 "smol",
 "stacked_errors",
 "tgbot",
]







|


>



<
<







1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788


1789
1790
1791
1792
1793
1794
1795
 "async-process",
 "blocking",
 "futures-lite",
]

[[package]]
name = "smtp2tg"
version = "0.6.7"
dependencies = [
 "async-compat",
 "clap",
 "config",
 "hostname",
 "html-escape",


 "mail-parser",
 "mailin-embedded",
 "regex",
 "smol",
 "stacked_errors",
 "tgbot",
]
2019
2020
2021
2022
2023
2024
2025






2026
2027
2028
2029
2030
2031
2032

[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"







[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
 "same-file",







>
>
>
>
>
>







2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145

[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"

[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"

[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
 "same-file",
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[package]
name = "smtp2tg"
version = "0.6.6"
authors = [ "arcade@b1t.name" ]
edition = "2024"
license = "0BSD"
repository = "http://fs.b1t.name/smtp2tg"

[dependencies]
async-compat = "0.2.5"
config = { version = "0.15", default-features = false, features = [ "toml" ] }
hostname = "0.4.1"
html-escape = "0.2.13"
just-getopt = "2.0.0"
lazy_static = "1.5.0"
mail-parser = { version = "0.11", features = ["serde"] }
mailin-embedded = "0.8"
regex = "1.11.1"
smol = "2.0.2"
stacked_errors = "0.7.1"
tgbot = "0.46"



|










|
<







1
2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
[package]
name = "smtp2tg"
version = "0.6.7"
authors = [ "arcade@b1t.name" ]
edition = "2024"
license = "0BSD"
repository = "http://fs.b1t.name/smtp2tg"

[dependencies]
async-compat = "0.2.5"
config = { version = "0.15", default-features = false, features = [ "toml" ] }
hostname = "0.4.1"
html-escape = "0.2.13"
clap = { version = "4.6", features = [ "derive" ] }

mail-parser = { version = "0.11", features = ["serde"] }
mailin-embedded = "0.8"
regex = "1.11.1"
smol = "2.0.2"
stacked_errors = "0.7.1"
tgbot = "0.46"










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









pub mod mail;
mod telegram;
pub mod utils;

// As we are not actually exporting this lib there would be no local Error's for
// now, everything will be just .stack()?'ed and propagated like in real bin
// The lib here is just to separate all tests from main code into tests/

use crate::mail::MailServer;

use std::{
	io::Cursor,
	os::unix::fs::PermissionsExt,
	path::Path,
};

use just_getopt::{
	OptFlags,
	OptSpecs,
	OptValue,
};
use smol::{
	fs::metadata,
};
use stacked_errors::{
	Result,
	StackableErr,
	bail,
};

/// 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\
>
>
>
>
>
>
>
>
>




<
<
<
<








|
<
<
<
<









|
|
|
|
|
|
<
<
<
|
<
<
|
|
|
>
>
>
>
>
|
<
|
>
|
|
<
|
<
<
<
<







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
//! SMTP-to-Telegram gateway main library.
//!
//! This module provides the core functionality for receiving emails via SMTP
//! and forwarding them to Telegram chats.
//!
//! As we are not actually exporting this lib there would be no local Error's
//! for now, everything will be just .stack()?'ed and propagated like in real
//! bin The lib here is just to separate all tests from main code into tests/

pub mod mail;
mod telegram;
pub mod utils;





use crate::mail::MailServer;

use std::{
	io::Cursor,
	os::unix::fs::PermissionsExt,
	path::Path,
};

use clap::Parser;




use smol::{
	fs::metadata,
};
use stacked_errors::{
	Result,
	StackableErr,
	bail,
};

/// SMTP-to-Telegram gateway
#[derive(Parser, Debug)]
#[command(name = "smtp2tg")]
#[command(about = format!("SMTP-to-Telegram gateway v{}, (C) 2024 - 2026", env!("CARGO_PKG_VERSION")), long_about = None)]
struct Args {
	/// Set configuration file location



	#[arg(short, long, default_value = "smtp2tg.toml")]


	config: String,
}

/// Main asynchronous entry point for the application.
///
/// Parses command-line arguments, loads configuration, and starts the SMTP
/// server.
///
/// # Errors

/// Returns an error if configuration is invalid, files are inaccessible, or
/// server fails to start.
pub async fn async_main () -> Result<()> {
	let args = Args::parse();

	let config_file = Path::new(&args.config);




	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\





1
2
3
4
5
6
7





use crate::{
	Cursor,
	telegram::TelegramTransport,
	utils::{
		Attachment,
		RE_DOMAIN,
		validate,
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
//! SMTP server implementation for receiving and processing emails.
//!
//! This module handles SMTP connections, email parsing, and forwarding to
//! Telegram.

use crate::{
	Cursor,
	telegram::TelegramTransport,
	utils::{
		Attachment,
		RE_DOMAIN,
		validate,
53
54
55
56
57
58
59






60

61
62
63
64
65
66
67
	relay: bool,
	tg: Arc<TelegramTransport>,
	fields: HashSet<String>,
	address: Regex,
}

impl MailServer {






	/// Initialize API and read configuration

	pub fn new(settings: config::Config) -> Result<MailServer> {
		let api_key = settings.get_string("api_key")
			.context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?;
		let mut recipients = HashMap::new();
		for (name, value) in settings.get_table("recipients")
			.expect("[smtp2tg.toml] missing table \"recipients\".\n")
		{







>
>
>
>
>
>
|
>







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
	relay: bool,
	tg: Arc<TelegramTransport>,
	fields: HashSet<String>,
	address: Regex,
}

impl MailServer {
	/// Main asynchronous entry point for the application.
	///
	/// Parses command-line arguments, loads configuration, and starts the SMTP
	/// server.
	///
	/// # Errors
	/// Returns an error if configuration is invalid, files are inaccessible, or
	/// server fails to start.
	pub fn new(settings: config::Config) -> Result<MailServer> {
		let api_key = settings.get_string("api_key")
			.context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?;
		let mut recipients = HashMap::new();
		for (name, value) in settings.get_table("recipients")
			.expect("[smtp2tg.toml] missing table \"recipients\".\n")
		{
103
104
105
106
107
108
109






110


111
112
113
114
115
116
117
			relay,
			tg,
			fields,
			address,
		})
	}







	/// Returns id for provided email address


	pub fn get_id (&self, name: &str) -> Result<&ChatPeerId> {
		if self.address.is_match(name) {
			match self.tg.get(name) {
				Ok(addr) => Ok(addr),
				Err(_) => Ok(&self.tg.default),
			}
		} else {







>
>
>
>
>
>
|
>
>







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
			relay,
			tg,
			fields,
			address,
		})
	}

	/// Retrieves the Telegram chat ID for a given email address, checks that
	/// used domain is allowed.
	///
	/// # Arguments
	/// * `name_str` - Email address or username to look up.
	///
	/// # Returns
	/// * `Result<ChatPeerId>` - Telegram chat ID for the address, or default if
	///   not found.
	pub fn get_id (&self, name: &str) -> Result<&ChatPeerId> {
		if self.address.is_match(name) {
			match self.tg.get(name) {
				Ok(addr) => Ok(addr),
				Err(_) => Ok(&self.tg.default),
			}
		} else {
256
257
258
259
260
261
262

263
264
265
266
267
268
269
		} else {
			bail!("Required headers were not found.");
		}
		Ok(())
	}
}


impl mailin_embedded::Handler for MailServer {
	/// Just deny login auth
	fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
		INVALID_CREDENTIALS
	}

	/// Just deny plain auth







>







276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
		} else {
			bail!("Required headers were not found.");
		}
		Ok(())
	}
}

/// SMTP handler implementation for mailin-embedded.
impl mailin_embedded::Handler for MailServer {
	/// Just deny login auth
	fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
		INVALID_CREDENTIALS
	}

	/// Just deny plain auth


1
2
3
4
5
6
7


use crate::utils::{
	Attachment,
	validate,
};

use std::{
	collections::HashMap,
>
>







1
2
3
4
5
6
7
8
9
//! Telegram API integration for sending messages and attachments.

use crate::utils::{
	Attachment,
	validate,
};

use std::{
	collections::HashMap,
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
pub struct TelegramTransport {
	tg: Client,
	recipients: HashMap<String, ChatPeerId>,
	pub default: ChatPeerId,
}

impl TelegramTransport {
	/// Creates new TelegramTransport object.








	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,
			default,
		})
	}

	/// Send message to default user, used for debug/log/info purposes






	pub async fn debug (&self, msg: &str) -> Result<Message> {
		self.send(&self.default, format!("<pre>{}</pre>", validate(msg).stack()?)).await
	}


	/// Get recipient by address





	pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
		self.recipients.get(name)
			.with_context(|| format!("Recipient \"{name}\" not found in configuration"))
	}

	/// Send message to specified user







	pub async fn send <S> (&self, to: &ChatPeerId, msg: S) -> Result<Message>
	where S: Into<String> + Debug{
		self.tg.execute(
			SendMessage::new(*to, msg)
			.with_parse_mode(Html)
		).await.stack()
	}

	/// Send media to specified user








	pub async fn sendgroup (&self, to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> {
		if media.len() > 1 {
			let mut attach = vec![];
			let mut pos = media.len();
			for file in media {
				let mut caption = InputMediaDocument::default();
				if pos == 1 {







|
>
>
>
>
>
>
>
>




















|
>
>
>
>
>
>




>
|
>
>
>
>
>





|
>
>
>
>
>
>
>








|
>
>
>
>
>
>
>
>







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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
pub struct TelegramTransport {
	tg: Client,
	recipients: HashMap<String, ChatPeerId>,
	pub default: ChatPeerId,
}

impl TelegramTransport {
	/// Creates a new `TelegramTransport` instance.
	///
	/// # Arguments
	/// * `api_key` - Telegram Bot API token.
	/// * `recipients` - Mapping of email addresses to Telegram chat IDs.
	/// * `settings` - Additional configuration (API gateway, default chat).
	///
	/// # Errors
	/// Returns an error if API client creation fails.
	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,
			default,
		})
	}

	/// Sends a debug message to the default chat.
	///
	/// # Arguments
	/// * `msg` - Message text to send.
	///
	/// # Returns
	/// * `Result<Message>` - Telegram API response.
	pub async fn debug (&self, msg: &str) -> Result<Message> {
		self.send(&self.default, format!("<pre>{}</pre>", validate(msg).stack()?)).await
	}

	/// Retrieves a chat ID by name.
	///
	/// # Arguments
	/// * `name` - Name or email to look up.
	///
	/// # Returns
	/// * `Result<&ChatPeerId>` - Chat ID if found.
	pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
		self.recipients.get(name)
			.with_context(|| format!("Recipient \"{name}\" not found in configuration"))
	}

	/// Sends a text message to a specified chat.
	///
	/// # Arguments
	/// * `to` - Target chat ID.
	/// * `msg` - Message text (supports HTML formatting).
	///
	/// # Returns
	/// * `Result<Message>` - Telegram API response.
	pub async fn send <S> (&self, to: &ChatPeerId, msg: S) -> Result<Message>
	where S: Into<String> + Debug{
		self.tg.execute(
			SendMessage::new(*to, msg)
			.with_parse_mode(Html)
		).await.stack()
	}

	/// Sends a message with attachments to a specified chat.
	///
	/// # Arguments
	/// * `to` - Target chat ID.
	/// * `media` - List of attachments.
	/// * `msg` - Message text (supports HTML formatting).
	///
	/// # Returns
	/// * `Result<()>` - Success or error.
	pub async fn sendgroup (&self, to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> {
		if media.len() > 1 {
			let mut attach = vec![];
			let mut pos = media.len();
			for file in media {
				let mut caption = InputMediaDocument::default();
				if pos == 1 {


1
2
3
4
5
6
7


use crate::Cursor;

use std::{
	borrow::Cow,
	sync::LazyLock,
};

>
>







1
2
3
4
5
6
7
8
9
//! Utility functions and types for the application.

use crate::Cursor;

use std::{
	borrow::Cow,
	sync::LazyLock,
};

28
29
30
31
32
33
34
35



36







37
38
39
40
41
42
43
/// while `name` holds the filename or display name of the attachment.
#[derive(Debug)]
pub struct Attachment {
	pub data: Cursor<Vec<u8>>,
	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 <'a>(text: &'a str) -> Result<Cow<'a, str>> {
	if RE_CLOSING.is_match(text) {
		bail!("Telegram closing tag found.");
	} else {
		Ok(encode_text(text))
	}
}







|
>
>
>
|
>
>
>
>
>
>
>







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
/// while `name` holds the filename or display name of the attachment.
#[derive(Debug)]
pub struct Attachment {
	pub data: Cursor<Vec<u8>>,
	pub name: String,
}

/// Validates text to ensure it doesn't break Telegram's preformatted blocks.
///
/// Escapes HTML special characters to prevent injection.
///
/// # Arguments
/// * `text` - Text to validate and escape.
///
/// # Returns
/// * `Result<Cow<'a, str>>` - Escaped text or error if invalid.
///
/// # Errors
/// Returns an error if the text contains Telegram closing tags (`</pre>`, `</code>`).
pub fn validate <'a>(text: &'a str) -> Result<Cow<'a, str>> {
	if RE_CLOSING.is_match(text) {
		bail!("Telegram closing tag found.");
	} else {
		Ok(encode_text(text))
	}
}