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