Overview
| Comment: | remove `unknown`, expand output and logs |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
0acb6536aec8d481fcebf9f3685de260 |
| User & Date: | arcade on 2026-09-10 07:06:19.036 |
| Other Links: | manifest | tags |
Context
|
2026-09-10
| ||
| 07:07 | bump check-in: cb1fdeff6f user: arcade tags: trunk | |
| 07:06 | remove `unknown`, expand output and logs check-in: 0acb6536ae user: arcade tags: trunk | |
|
2026-08-01
| ||
| 19:32 | and fix tests check-in: 40a93e9a58 user: arcade tags: trunk | |
Changes
Modified smtp2tg.toml.example
from [448e8e5d20]
to [02af9a5a15].
1 2 3 4 5 6 7 8 9 10 | # vi:ft=toml: # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" # Telegram API gateway (when you are running your own) api_gateway = "https://api.telegram.org" # <- note no trailing slash # where to listen on (sockets are not supported since 0.3.0) listen_on = "0.0.0.0:25" | < | < < | < < | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # vi:ft=toml: # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" # Telegram API gateway (when you are running your own) api_gateway = "https://api.telegram.org" # <- note no trailing slash # where to listen on (sockets are not supported since 0.3.0) listen_on = "0.0.0.0:25" # default hostname to use when serving requests hostname = "smtp2tg" # which domains are allowed in addresses # this means that any unqualified recipient "somebody" will also match # to "somebody@each_domain" domains = [ "localhost", "current.hostname" ] # default recipient, should be specified # still can be a user, channel or group id default = 0 # default fields to show in message header fields = [ "date", "from", "subject" ] [recipients] # make sure you quote emails, as "@" can't go there unquoted. And by default # we need FQDNs, also keep in mind emails are case insensitive "somebody@example.com" = 1 # user id's are positive "root" = -1 # group id's are negative # to look up chat/group id you can use debug settings in Telegram clients, # or some bot like @getidsbot or @RawDataBot |
Modified src/lib.rs
from [984fc307f8]
to [14885cdcbb].
| ︙ | ︙ | |||
47 48 49 50 51 52 53 |
/// # Errors
/// Returns an error if configuration is invalid, files are inaccessible, or
/// server fails to start.
pub async fn async_main () -> Result<()> {
let args = Args::parse();
let config_file = Path::new(&args.config);
if !config_file.exists() {
| | > > > > | | | < | > | > > | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
/// # Errors
/// Returns an error if configuration is invalid, files are inaccessible, or
/// server fails to start.
pub async fn async_main () -> Result<()> {
let args = Args::parse();
let config_file = Path::new(&args.config);
if !config_file.exists() {
bail!("Configuration file not found: {config_file:?}\n\
Hint: Ensure the file exists and the path is correct.");
};
{
let meta = metadata(config_file).await.stack()?;
if (!0o100600 & meta.permissions().mode()) > 0 {
bail!("Configuration file permissions are insecure {config_file:?}\n\
Current permissions: {:o}\n\
Required: 0600 (owner read/write only).\n\
Fix with: chmod 600 {config_file:?}",
meta.permissions().mode());
} }
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().context("Failed to get current hostname")?
.to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()?
.add_source(config::File::from(config_file))
.build()
.with_context(|| format!(
"Failed to parse configuration file: {config_file:?}\n\
Check syntax against smtp2tg.toml.example.\n\
Common issues: missing quotes, trailing commas, or invalid types."
))?;
let listen_on = settings.get_string("listen_on").stack()?;
let server_name = settings.get_string("hostname").stack()?;
let core = MailServer::new(settings)?;
let mut server = mailin_embedded::Server::new(core);
// TODO: remove unwraps when mailin-embedded bumps with better error handling
|
| ︙ | ︙ |
Modified src/mail.rs
from [478a14c8ba]
to [1c18fc67bc].
| ︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#[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
/// validates all required configuration values.
///
/// # Arguments
| > | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
#[derive(Clone, Debug)]
pub struct MailServer {
data: Vec<u8>,
headers: Option<SomeHeaders>,
tg: Arc<TelegramTransport>,
fields: HashSet<String>,
address: Regex,
domains: HashSet<String>,
}
impl MailServer {
/// Initializes the mail server: sets up the Telegram API client and
/// validates all required configuration values.
///
/// # Arguments
|
| ︙ | ︙ | |||
91 92 93 94 95 96 97 |
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 {
| > > | | < | | > | | < | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
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 {
bail!("Invalid domain in configuration: '{domain}'\n\
Domains must be valid (e.g., 'example.com', 'localhost').\n\
Check 'domains' array in smtp2tg.toml.");
} }
if domains.is_empty() {
bail!("No domains, need at least one: default `localhost` would do.");
}
let re_domains = domains.iter().map(|s| escape(s))
.collect::<Vec<String>>().join("|");
let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({re_domains}))?$"))
.case_insensitive(true).build().stack()?;
Ok(MailServer {
data: vec!(),
headers: None,
tg,
fields,
address,
domains,
})
}
/// 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!("Email address {name:?} is not from an allowed domain.");
} }
/// Attempt to deliver one message
async fn relay_mail (&self) -> Result<()> {
if let Some(headers) = &self.headers {
let mail = mail_parser::MessageParser::new().parse(&self.data)
.context("Failed to parse mail.")?;
|
| ︙ | ︙ |
Modified src/telegram.rs
from [b857ac5a83]
to [683b75dd50].
| ︙ | ︙ | |||
92 93 94 95 96 97 98 |
/// # 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())
| | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
/// # 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())
.with_context(|| format!("Recipient {name:?} not found in configuration"))
}
/// Sends a text message to a specified chat.
///
/// # Arguments
/// * `to` - Target chat ID.
/// * `msg` - Message text (supports HTML formatting).
|
| ︙ | ︙ |
Modified src/utils.rs
from [b825a3dd51]
to [999160a858].
| ︙ | ︙ | |||
44 45 46 47 48 49 50 |
/// # Returns
/// * `Result<Cow<'a, str>>` - Escaped text or error if invalid.
///
/// # Errors
/// Returns an error if the text contains Telegram closing tags (`</pre>`, `</code>`).
pub fn validate <'a>(text: &'a str) -> Result<Cow<'a, str>> {
if RE_CLOSING.is_match(text) {
| | | < | 44 45 46 47 48 49 50 51 52 53 54 |
/// # Returns
/// * `Result<Cow<'a, str>>` - Escaped text or error if invalid.
///
/// # Errors
/// Returns an error if the text contains Telegram closing tags (`</pre>`, `</code>`).
pub fn validate <'a>(text: &'a str) -> Result<Cow<'a, str>> {
if RE_CLOSING.is_match(text) {
bail!("Text contains a Telegram closing tag (e.g., </pre>, </code>).");
} else {
Ok(encode_text(text))
} }
|