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

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_gateway = "https://api.telegram.org"
listen_on = "127.0.0.1:1025"
unknown = "relay"
fields = ["date", "from", "subject"]
domains = ["example.com", "localhost"]

default = 0







|







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

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

---








>







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
## 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:**
```
From: user@example.com
To: admin@example.com
Subject: Test

Hello, world!
```

**Telegram message:**
```
<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)







|








|












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
	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")
		{
			let value = value.into_int()







|
>

<
|
>


|

|







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 {
	/// Initializes the mail server: sets up the Telegram API client and
	/// validates all required configuration values.
	///

	/// # Arguments
	/// * `settings` - Parsed application configuration.
	///
	/// # Errors
	/// Returns an error if required configuration values are missing or invalid.
	/// 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")
		{
			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
		})
	}

	/// 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 {
			bail!("Doesn't look like address from one of our domains.");
		}
	}

	/// Attempt to deliver one message
	async fn relay_mail (&self) -> Result<()> {







|






|
<
<
<







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` - 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) {
			Ok(self.tg.get(name).unwrap_or(&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
	/// 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 {
			OK
		} else {
			match self.get_id(to) {
				Ok(_) => OK,
				Err(_) => {
					if self.relay {
						OK
					} else {
						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(),







|


<
<
<
<
<
<
|
<
<
<







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 || self.get_id(to).is_ok() {
			OK
		} else {






			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
//! Telegram API integration for sending messages and attachments.

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

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

use stacked_errors::{

	Result,
	StackableErr,
};
use tgbot::{
	api::Client,
	types::{
		ChatPeerId,













>







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
	///
	/// # 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)







>
|







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







>
>
>











>
>
>







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
		).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![];







|







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, 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
						),
						caption
					)
				);
			}
			self.tg.execute(SendMediaGroup::new(*to, MediaGroup::new(attach).stack()?)).await.stack()?;
		} else {



			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(())
	}
}







>
>
>












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(())
	}
}