Check-in [1723b63d69]
Logged in as anonymous
Overview
Comment:doc corrections/optimizations
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 1723b63d6980199b9bd26a6af71be2046eb67eeca8b92d4ba5ed91bc48e2381c
User & Date: arcade on 2026-08-01 13:46:06.271
Other Links: manifest | tags
Context
2026-08-01
15:30
fix regexp, get rid of relaying variants as they are actually noop after move to mailin check-in: 158c9cffc6 user: arcade tags: trunk
13:46
doc corrections/optimizations check-in: 1723b63d69 user: arcade tags: trunk
11:30
remove serde dep for mail-parser check-in: b9bf773618 user: arcade tags: trunk
Changes
Modified README.md from [438799e731] to [3b91b4e83f].
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31







-
+








1. Create `smtp2tg.toml` (see `smtp2tg.toml.example` for reference).
2. Get a Telegram Bot Token from [@BotFather](https://t.me/BotFather).
3. Get your chat ID (use [@getidsbot](https://t.me/getidsbot) or debug mode in Telegram client).

### Example configuration
```toml
api_key = "123456789ABCdefGHIjklMNOpq"
api_key = "replace-with-your-telegram-bot-token"
api_gateway = "https://api.telegram.org"
listen_on = "127.0.0.1:1025"
unknown = "relay"
fields = ["date", "from", "subject"]
domains = ["example.com", "localhost"]

default = 0
54
55
56
57
58
59
60

61
62
63
64
65
66
67
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68







+








### Run
```bash
./smtp2tg -c /path/to/smtp2tg.toml
```

### CLI arguments

| Argument      | Description               | Example               |
|---------------|---------------------------|-----------------------|
| `-h`, `--help` | Show help                 | `smtp2tg --help`      |
| `-c`, `--config` | Path to config file     | `smtp2tg -c config.toml` |

---

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







-
+








-
+












## How it works
1. A client (e.g., Postfix) sends an email to `listen_on` (e.g., `127.0.0.1:1025`).
2. smtp2tg parses the email and converts it to a Telegram message.
3. The message is sent to the specified chat (or `default` if address is unknown).

### Example: Email → Telegram
**Incoming email:**
```
```text
From: user@example.com
To: admin@example.com
Subject: Test

Hello, world!
```

**Telegram message:**
```
```html
<blockquote expandable>
<u><i>Subject:</i></u> <code>Test</code>
<u><i>From:</i></u> <code>user@example.com</code>
<u><i>Date:</i></u> <code>Mon, 01 Jan 2024 12:00:00 +0000</code>
</blockquote>
<pre>Hello, world!</pre>
```

---
## Links
- **Original repository**: [http://fs.b1t.name/smtp2tg](http://fs.b1t.name/smtp2tg)
- **GitHub Mirror**: [https://github.com/kworr/smtp2tg](https://github.com/kworr/smtp2tg)
58
59
60
61
62
63
64
65


66
67
68


69
70
71

72
73

74
75
76
77
78
79
80
58
59
60
61
62
63
64

65
66
67


68
69
70
71

72
73

74
75
76
77
78
79
80
81







-
+
+

-
-
+
+


-
+

-
+







	relay: bool,
	tg: Arc<TelegramTransport>,
	fields: HashSet<String>,
	address: Regex,
}

impl MailServer {
	/// Main asynchronous entry point for the application.
	/// Initializes the mail server: sets up the Telegram API client and
	/// validates all required configuration values.
	///
	/// Parses command-line arguments, loads configuration, and starts the SMTP
	/// server.
	/// # Arguments
	/// * `settings` - Parsed application configuration.
	///
	/// # Errors
	/// Returns an error if configuration is invalid, files are inaccessible, or
	/// Returns an error if required configuration values are missing or invalid.
	/// server fails to start.
	pub fn new(settings: config::Config) -> Result<MailServer> {
	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")
		{
			let value = value.into_int()
119
120
121
122
123
124
125
126

127
128
129
130
131
132
133

134
135
136
137
138
139
140
141
142
143
120
121
122
123
124
125
126

127
128
129
130
131
132
133

134



135
136
137
138
139
140
141







-
+






-
+
-
-
-







		})
	}

	/// 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.
	/// * `name` - 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(self.tg.get(name).unwrap_or(&self.tg.default))
				Ok(addr) => Ok(addr),
				Err(_) => Ok(&self.tg.default),
			}
		} else {
			bail!("Doesn't look like address from one of our domains.");
		}
	}

	/// Attempt to deliver one message
	async fn relay_mail (&self) -> Result<()> {
290
291
292
293
294
295
296
297

298
299
300
301
302
303
304
305
306

307
308
309
310
311
312
313
314
315
316
288
289
290
291
292
293
294

295
296
297







298



299
300
301
302
303
304
305







-
+


-
-
-
-
-
-
-
+
-
-
-







	/// Just deny plain auth
	fn auth_plain (&mut self, _authorization_id: &str, _authentication_id: &str, _password: &str) -> Response {
		INVALID_CREDENTIALS
	}

	/// Verify whether address is deliverable
	fn rcpt (&mut self, to: &str) -> Response {
		if self.relay {
		if self.relay || self.get_id(to).is_ok() {
			OK
		} else {
			match self.get_id(to) {
				Ok(_) => OK,
				Err(_) => {
					if self.relay {
						OK
					} else {
						NO_MAILBOX
			NO_MAILBOX
					}
				}
			}
		}
	}

	/// Save headers we need
	fn data_start (&mut self, _domain: &str, from: &str, _is8bit: bool, to: &[String]) -> Response {
		self.headers = Some(SomeHeaders{
			from: from.to_string(),
1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21













+







//! Telegram API integration for sending messages and attachments.

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

use std::{
	collections::HashMap,
	fmt::Debug,
};

use stacked_errors::{
	bail,
	Result,
	StackableErr,
};
use tgbot::{
	api::Client,
	types::{
		ChatPeerId,
43
44
45
46
47
48
49

50

51
52
53
54
55
56
57
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59







+
-
+







	///
	/// # 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 configuration values cannot be read or if Telegram
	/// Returns an error if API client creation fails.
	/// 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)
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
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







+
+
+











+
+
+







	/// Sends a debug message to the default chat.
	///
	/// # Arguments
	/// * `msg` - Message text to send.
	///
	/// # Returns
	/// * `Result<Message>` - Telegram API response.
	///
	/// # Errors
	/// Returns an error if `msg` contains a closing Telegram tag or sending fails.
	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.
	///
	/// # Errors
	/// Returns an error if `name` is not configured.
	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.
	///
107
108
109
110
111
112
113
114

115
116
117
118
119
120
121
115
116
117
118
119
120
121

122
123
124
125
126
127
128
129







-
+







		).await.stack()
	}

	/// Sends a message with attachments to a specified chat.
	///
	/// # Arguments
	/// * `to` - Target chat ID.
	/// * `media` - List of attachments.
	/// * `media` - List of attachments, non-empty.
	/// * `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![];
135
136
137
138
139
140
141



142
143
144
145
146
147
148
149
150
151
152
153
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164







+
+
+












						),
						caption
					)
				);
			}
			self.tg.execute(SendMediaGroup::new(*to, MediaGroup::new(attach).stack()?)).await.stack()?;
		} else {
			if media.is_empty() {
				bail!("At least one attachment is required.");
			}
			self.tg.execute(
				SendDocument::new(
					*to,
					InputFileReader::from(media[0].data.clone())
					.with_file_name(media[0].name.clone())
				).with_caption(msg)
				.with_caption_parse_mode(Html)
			).await.stack()?;
		}
		Ok(())
	}
}