Overview
| Comment: | fix regexp, get rid of relaying variants as they are actually noop after move to mailin |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
158c9cffc69ede917df67448e11064ef |
| User & Date: | arcade on 2026-08-01 15:30:38.399 |
| Other Links: | manifest | tags |
Context
|
2026-08-01
| ||
| 18:47 | ensure!() we don't panic but instead return Result whenever possible, convert error handling a little, add TODO for unwrap(), more sanity checks, simplify and expand testing check-in: 98c5a42df0 user: arcade tags: trunk | |
| 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 | |
Changes
Modified README.md
from [3b91b4e83f]
to [4b74daffcc].
| ︙ | ︙ | |||
20 21 22 23 24 25 26 | 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" | < < < < < < < < < < < < < < | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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" fields = ["date", "from", "subject"] domains = ["example.com", "localhost"] default = 0 [recipients] "admin@example.com" = 12345678 "alerts@example.com" = -10012345678 ``` --- ## Usage ### Run ```bash ./smtp2tg -c /path/to/smtp2tg.toml |
| ︙ | ︙ |
Modified src/lib.rs
from [7f35ba3c51]
to [3665254a0d].
| ︙ | ︙ | |||
61 62 63 64 65 66 67 |
}
}
let settings: config::Config = config::Config::builder()
.set_default("api_gateway", "https://api.telegram.org").stack()?
.set_default("fields", vec!["date", "from", "subject"]).stack()?
.set_default("hostname", "smtp.2.tg").stack()?
.set_default("listen_on", "0.0.0.0:1025").stack()?
| < | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
}
}
let settings: config::Config = config::Config::builder()
.set_default("api_gateway", "https://api.telegram.org").stack()?
.set_default("fields", vec!["date", "from", "subject"]).stack()?
.set_default("hostname", "smtp.2.tg").stack()?
.set_default("listen_on", "0.0.0.0:1025").stack()?
.set_default("domains", vec!["localhost",
hostname::get().expect("Failed to get current hostname")
.to_str().expect("Can't convert hostname to string, bad UTF-8?")]).stack()?
.add_source(config::File::from(config_file))
.build()
.with_context(|| format!("[{config_file:?}] there was an error reading config\n\
\tplease consult \"smtp2tg.toml.example\" for details"))?;
|
| ︙ | ︙ |
Modified src/mail.rs
from [3f2e34fec5]
to [eb64cf1123].
| ︙ | ︙ | |||
30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
INVALID_CREDENTIALS,
NO_MAILBOX,
OK
},
};
use regex::{
Regex,
escape,
};
use stacked_errors::{
Result,
StackableErr,
bail,
};
| > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
INVALID_CREDENTIALS,
NO_MAILBOX,
OK
},
};
use regex::{
Regex,
RegexBuilder,
escape,
};
use stacked_errors::{
Result,
StackableErr,
bail,
};
|
| ︙ | ︙ | |||
51 52 53 54 55 56 57 |
}
/// `MailServer` Central object with TG api and configuration
#[derive(Clone, Debug)]
pub struct MailServer {
data: Vec<u8>,
headers: Option<SomeHeaders>,
| < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
}
/// `MailServer` Central object with TG api and configuration
#[derive(Clone, Debug)]
pub struct MailServer {
data: Vec<u8>,
headers: Option<SomeHeaders>,
tg: Arc<TelegramTransport>,
fields: HashSet<String>,
address: Regex,
}
impl MailServer {
/// Initializes the mail server: sets up the Telegram API client and
|
| ︙ | ︙ | |||
76 77 78 79 80 81 82 |
.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()
.context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
| | | < < < | < < < < < < | 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 104 105 106 107 108 109 110 111 112 113 114 |
.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()
.context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
recipients.insert(name.to_lowercase().replace('.', ""), value);
}
let tg = Arc::new(TelegramTransport::new(api_key, recipients, &settings)?);
let fields = HashSet::<String>::from_iter(settings.get_array("fields")
.expect("[smtp2tg.toml] \"fields\" should be an array")
.iter().map(|x| x.clone().into_string().expect("should be strings")));
let mut domains: HashSet<String> = HashSet::new();
let extra_domains = settings.get_array("domains").stack()?;
for domain in extra_domains {
let domain = domain.to_string().to_lowercase();
if RE_DOMAIN.is_match(&domain) {
domains.insert(domain);
} else {
panic!("[smtp2tg.toml] can't check of domains in \"domains\": {domain}");
}
}
let domains = domains.into_iter().map(|s| escape(&s))
.collect::<Vec<String>>().join("|");
let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({domains}))?$"))
.case_insensitive(true).build().stack()?;
Ok(MailServer {
data: vec!(),
headers: None,
tg,
fields,
address,
})
}
/// Retrieves the Telegram chat ID for a given email address, checks that
|
| ︙ | ︙ | |||
142 143 144 145 146 147 148 |
if let Some(headers) = &self.headers {
let mail = mail_parser::MessageParser::new().parse(&self.data)
.context("Failed to parse mail.")?;
// Adding all known addresses to recipient list, for anyone else adding default
// Also if list is empty also adding default
let mut rcpt: HashSet<&ChatPeerId> = HashSet::new();
| | | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
if let Some(headers) = &self.headers {
let mail = mail_parser::MessageParser::new().parse(&self.data)
.context("Failed to parse mail.")?;
// Adding all known addresses to recipient list, for anyone else adding default
// Also if list is empty also adding default
let mut rcpt: HashSet<&ChatPeerId> = HashSet::new();
if headers.to.is_empty() {
bail!("Relaying is disabled, and there's no destination address");
}
for item in &headers.to {
rcpt.insert(self.get_id(item)?);
};
if rcpt.is_empty() {
self.tg.debug("No recipient or envelope address.").await?;
|
| ︙ | ︙ | |||
288 289 290 291 292 293 294 |
/// 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 {
| | | 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
/// 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.get_id(to).is_ok() {
OK
} else {
NO_MAILBOX
}
}
/// Save headers we need
|
| ︙ | ︙ |
Modified src/telegram.rs
from [4f9a819f27]
to [13cd441b91].
| ︙ | ︙ | |||
91 92 93 94 95 96 97 |
///
/// # Returns
/// * `Result<&ChatPeerId>` - Chat ID if found.
///
/// # Errors
/// Returns an error if `name` is not configured.
pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
| | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
///
/// # 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.to_lowercase().replace('.', ""))
.with_context(|| format!("Recipient \"{name}\" not found in configuration"))
}
/// Sends a text message to a specified chat.
///
/// # Arguments
/// * `to` - Target chat ID.
|
| ︙ | ︙ |
Modified tests/mail.rs
from [32cfc4300b]
to [5b6e412716].
| ︙ | ︙ | |||
10 11 12 13 14 15 16 |
/// network access is performed while constructing it.
fn build_server () -> Result<MailServer> {
let settings = config::Config::builder()
.add_source(config::File::from_str(r#"
api_key = "test-api-key"
api_gateway = "https://api.telegram.org"
default = 0
| < | > > > > > > > > > > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/// network access is performed while constructing it.
fn build_server () -> Result<MailServer> {
let settings = config::Config::builder()
.add_source(config::File::from_str(r#"
api_key = "test-api-key"
api_gateway = "https://api.telegram.org"
default = 0
fields = ["date", "from", "subject"]
domains = ["example.com"]
[recipients]
"someone@example.com" = 1
"root" = -1
"#, config::FileFormat::Toml))
.build()
.stack()?;
MailServer::new(settings)
}
#[test]
fn get_id_properly_resolves_addresses () -> Result<()> {
let server = build_server()?;
let cases = [
("someone@example.com", 1),
("someone", 0),
("root", -1),
("unknown@example.com", 0),
("SOMEONE@example.com", 1), // uppercase local part
("someone@EXAMPLE.COM", 1), // uppercase domain
("some.one@example.com", 1), // functionally equivalent to skipping '.'
("some-one-2", 0), // Hyphens
];
for (email, id) in cases {
assert_eq!(*server.get_id(email)?, ChatPeerId::from(id), "email [{email}] expected to return id [{id}]");
}
let cases = [
"someone@otherdomain.net",
"@example.com", // empty local part
"some@one@example.com", // more than one '@'
"someone@example.com.evil",
"someone@example.org",
];
for email in cases {
let err = server.get_id(email).err()
.ok_or_else(|| format!("email [{email}] expected to fail")).stack()?;
assert!(err.to_string().contains("Doesn't look like address from one of our domains."));
}
Ok(())
}
|