Lines of
src/lib.rs
from check-in e678fcdc39
that are changed by the sequence of edits moving toward
check-in ac6ac8c108:
1: //! SMTP-to-Telegram gateway main library.
2: //!
e678fcdc39 2026-09-12 3: //! This module provides the core functionality for receiving emails via SMTP
4: //! and forwarding them to Telegram chats.
5: //!
6: //! As we are not actually exporting this lib there would be no local Error's
7: //! for now, everything will be just .stack()?'ed and propagated like in real
e678fcdc39 2026-09-12 8: //! bin The lib here is just to separate all tests from main code into tests/
9:
10: pub mod mail;
11: mod telegram;
12: pub mod utils;
13:
e678fcdc39 2026-09-12 14: use crate::mail::MailServer;
e678fcdc39 2026-09-12 15:
e678fcdc39 2026-09-12 16: use std::{
e678fcdc39 2026-09-12 17: io::Cursor,
e678fcdc39 2026-09-12 18: os::unix::fs::PermissionsExt,
e678fcdc39 2026-09-12 19: path::Path,
e678fcdc39 2026-09-12 20: };
e678fcdc39 2026-09-12 21:
e678fcdc39 2026-09-12 22: use clap::Parser;
e678fcdc39 2026-09-12 23: use smol::{
e678fcdc39 2026-09-12 24: fs::metadata,
e678fcdc39 2026-09-12 25: };
e678fcdc39 2026-09-12 26: use stacked_errors::{
e678fcdc39 2026-09-12 27: Result,
e678fcdc39 2026-09-12 28: StackableErr,
e678fcdc39 2026-09-12 29: bail,
e678fcdc39 2026-09-12 30: };
e678fcdc39 2026-09-12 31:
e678fcdc39 2026-09-12 32: /// SMTP-to-Telegram gateway
e678fcdc39 2026-09-12 33: #[derive(Parser, Debug)]
e678fcdc39 2026-09-12 34: #[command(name = "smtp2tg")]
e678fcdc39 2026-09-12 35: #[command(about = format!("SMTP-to-Telegram gateway v{}, (C) 2024 - 2026", env!("CARGO_PKG_VERSION")), long_about = None)]
e678fcdc39 2026-09-12 36: struct Args {
e678fcdc39 2026-09-12 37: /// Set configuration file location
e678fcdc39 2026-09-12 38: #[arg(short, long, default_value = "smtp2tg.toml")]
e678fcdc39 2026-09-12 39: config: String,
e678fcdc39 2026-09-12 40: }
e678fcdc39 2026-09-12 41:
e678fcdc39 2026-09-12 42: /// Main asynchronous entry point for the application. Runs the gateway using
e678fcdc39 2026-09-12 43: /// the configuration selected on the command line.
e678fcdc39 2026-09-12 44: ///
e678fcdc39 2026-09-12 45: /// The configuration file must use owner-only permissions. Once configured,
e678fcdc39 2026-09-12 46: /// the SMTP server runs until it stops or encounters an error.
e678fcdc39 2026-09-12 47: ///
e678fcdc39 2026-09-12 48: /// # Errors
e678fcdc39 2026-09-12 49: /// Returns an error if the configuration file is missing, inaccessible,
e678fcdc39 2026-09-12 50: /// insecure, malformed, or contains invalid required settings.
e678fcdc39 2026-09-12 51: ///
e678fcdc39 2026-09-12 52: /// # Panics
e678fcdc39 2026-09-12 53: /// Panics if configuring or serving the SMTP server fails.
e678fcdc39 2026-09-12 54: pub async fn async_main () -> Result<()> {
e678fcdc39 2026-09-12 55: let args = Args::parse();
e678fcdc39 2026-09-12 56: let config_file = Path::new(&args.config);
e678fcdc39 2026-09-12 57: if !config_file.try_exists().stack()? {
e678fcdc39 2026-09-12 58: bail!("Configuration file not found: {config_file:?}\n\
e678fcdc39 2026-09-12 59: Hint: Ensure the file exists and the path is correct.");
e678fcdc39 2026-09-12 60: };
e678fcdc39 2026-09-12 61: {
e678fcdc39 2026-09-12 62: let meta = metadata(config_file).await.stack()?;
e678fcdc39 2026-09-12 63: if (!0o100600 & meta.permissions().mode()) > 0 {
e678fcdc39 2026-09-12 64: bail!("Configuration file permissions are insecure {config_file:?}\n\
e678fcdc39 2026-09-12 65: Current permissions: {:o}\n\
e678fcdc39 2026-09-12 66: Required: 0600 (owner read/write only).\n\
e678fcdc39 2026-09-12 67: Fix with: chmod 600 {config_file:?}",
e678fcdc39 2026-09-12 68: meta.permissions().mode());
e678fcdc39 2026-09-12 69: } }
e678fcdc39 2026-09-12 70: let settings: config::Config = config::Config::builder()
e678fcdc39 2026-09-12 71: .set_default("api_gateway", "https://api.telegram.org").stack()?
e678fcdc39 2026-09-12 72: .set_default("fields", vec!["date", "from", "subject"]).stack()?
e678fcdc39 2026-09-12 73: .set_default("hostname", "smtp.2.tg").stack()?
e678fcdc39 2026-09-12 74: .set_default("listen_on", "0.0.0.0:1025").stack()?
e678fcdc39 2026-09-12 75: .set_default("domains", vec!["localhost",
e678fcdc39 2026-09-12 76: hostname::get().context("Failed to get current hostname")?
e678fcdc39 2026-09-12 77: .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()?
e678fcdc39 2026-09-12 78: .add_source(config::File::from(config_file))
e678fcdc39 2026-09-12 79: .build()
e678fcdc39 2026-09-12 80: .with_context(|| format!(
e678fcdc39 2026-09-12 81: "Failed to parse configuration file: {config_file:?}\n\
e678fcdc39 2026-09-12 82: Check syntax against smtp2tg.toml.example.\n\
e678fcdc39 2026-09-12 83: Common issues: missing quotes, trailing commas in inline tables, or invalid types."
e678fcdc39 2026-09-12 84: ))?;
e678fcdc39 2026-09-12 85:
e678fcdc39 2026-09-12 86: let listen_on = settings.get_string("listen_on").stack()?;
e678fcdc39 2026-09-12 87: let server_name = settings.get_string("hostname").stack()?;
e678fcdc39 2026-09-12 88: let core = MailServer::new(settings)?;
e678fcdc39 2026-09-12 89: let mut server = mailin_embedded::Server::new(core);
e678fcdc39 2026-09-12 90:
e678fcdc39 2026-09-12 91: // TODO: remove unwraps when mailin-embedded bumps with better error handling
e678fcdc39 2026-09-12 92: server.with_name(server_name)
e678fcdc39 2026-09-12 93: .with_ssl(mailin_embedded::SslConfig::None).unwrap()
e678fcdc39 2026-09-12 94: .with_addr(listen_on).unwrap();
e678fcdc39 2026-09-12 95: server.serve().unwrap();
e678fcdc39 2026-09-12 96:
e678fcdc39 2026-09-12 97: Ok(())
e678fcdc39 2026-09-12 98: }