Overview
| Comment: | doc corrections/optimizations |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
1723b63d6980199b9bd26a6af71be204 |
| 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 | 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 | | | 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 | ## 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:** | | | | 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) |
Modified src/mail.rs
from [48b0e1b904]
to [3f2e34fec5].
| ︙ | ︙ | |||
58 59 60 61 62 63 64 |
relay: bool,
tg: Arc<TelegramTransport>,
fields: HashSet<String>,
address: Regex,
}
impl MailServer {
| | > < | > | | | 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 | }) } /// Retrieves the Telegram chat ID for a given email address, checks that /// used domain is allowed. /// /// # Arguments | | | < < < | 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 |
/// 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 {
| | < < < < < < | < < < | 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(),
|
| ︙ | ︙ |
Modified src/telegram.rs
from [5758349194]
to [4f9a819f27].
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 | /// /// # Arguments /// * `api_key` - Telegram Bot API token. /// * `recipients` - Mapping of email addresses to Telegram chat IDs. /// * `settings` - Additional configuration (API gateway, default chat). /// /// # Errors | > | | 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 | ).await.stack() } /// Sends a message with attachments to a specified chat. /// /// # Arguments /// * `to` - Target chat ID. | | | 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(())
}
}
|