Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -19,11 +19,11 @@
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"]
@@ -56,10 +56,11 @@
```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` |
@@ -86,20 +87,20 @@
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
Subject: Test
From: user@example.com
Date: Mon, 01 Jan 2024 12:00:00 +0000
Index: src/mail.rs
==================================================================
--- src/mail.rs
+++ src/mail.rs
@@ -60,19 +60,20 @@
fields: HashSet,
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 {
+ pub fn new (settings: config::Config) -> Result {
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")
@@ -121,21 +122,18 @@
/// 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` - 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),
- }
+ Ok(self.tg.get(name).unwrap_or(&self.tg.default))
} else {
bail!("Doesn't look like address from one of our domains.");
}
}
@@ -292,23 +290,14 @@
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 {
Index: src/telegram.rs
==================================================================
--- src/telegram.rs
+++ src/telegram.rs
@@ -9,10 +9,11 @@
collections::HashMap,
fmt::Debug,
};
use stacked_errors::{
+ bail,
Result,
StackableErr,
};
use tgbot::{
api::Client,
@@ -45,11 +46,12 @@
/// * `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.
+ /// Returns an error if configuration values cannot be read or if Telegram
+ /// API client creation fails.
pub fn new (api_key: String, recipients: HashMap, settings: &config::Config) -> Result {
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")?;
@@ -73,10 +75,13 @@
/// # Arguments
/// * `msg` - Message text to send.
///
/// # Returns
/// * `Result` - 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 {
self.send(&self.default, format!("{}", validate(msg).stack()?)).await
}
/// Retrieves a chat ID by name.
@@ -84,10 +89,13 @@
/// # 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"))
}
@@ -109,11 +117,11 @@
/// 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, msg: &str) -> Result<()> {
@@ -137,10 +145,13 @@
)
);
}
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())