ADDED LICENSE.0BSD Index: LICENSE.0BSD ================================================================== --- /dev/null +++ LICENSE.0BSD @@ -0,0 +1,12 @@ +Copyright (C) 2024 by Volodymyr Kostyrko + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. ADDED README Index: README ================================================================== --- /dev/null +++ README @@ -0,0 +1,16 @@ +First of all make sure to never listen on public IPs and always put sockets in +directories not acessible by just about everyone on your system. Though +listening on 127.0.0.1:XXXX is equivalent to creating a world-writable socket. + +To catch bounces (so they wouldn't stuck in upper mail server) make sure sender +envelope address is real as required by SaMoToP. For example Postfix has to be +tweaked like this: + +$config_directory/main.cf: + smtp_generic_maps = hash:$config_directory/generic + +$config_directory/generic: + "" postmaster@example.com + <> postmaster@example.com + +Actually not sure which one works... Index: smtp2tg.toml.example ================================================================== --- smtp2tg.toml.example +++ smtp2tg.toml.example @@ -1,17 +1,17 @@ # Telegram API key api_key = "YOU_KNOW_WHERE_TO_GET_THIS" # where SaMoToP stores incoming messages maildir = "./maildir" -# default recipient, get's some debug info + mail that we couldn't deliver -# should be in "recipients" table -default = "somebody@example.com" # where to listen on, say "socket" to listen on "./smtp2tg.sock" #listen_on = "0.0.0.0:25" listen_on = "socket" [recipients] +# there should be default recipient, get's some debug info + mail that we +# couldn't deliver +_ = 1 # make sure you quote emails, as "@" can't go there unquoted. And by default # we need FQDNs "somebody@example.com" = 1 # user id's are positive "root@example.com" = -1 # group id's are negative Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -24,10 +24,14 @@ collections::{ HashMap, HashSet, }, io::Read, + os::unix::fs::{ + FileTypeExt, + PermissionsExt, + }, path::{ Path, PathBuf }, time::Duration, @@ -71,23 +75,24 @@ } }; // Adding all known addresses to recipient list, for anyone else adding default // Also if list is empty also adding default - let mut rcpt: HashSet = HashSet::new(); + let mut rcpt: HashSet<&UserId> = HashSet::new(); for item in to { let item = item.into_owned(); - if core.recipients.contains_key(&item) { - rcpt.insert(core.recipients[&item]); - } else { - core.debug(format!("Recipient [{}] not found.", &item)).await.unwrap(); - rcpt.insert(core.default); - } + match core.recipients.get(&item) { + Some(addr) => rcpt.insert(addr), + None => { + core.debug(format!("Recipient [{}] not found.", &item)).await.unwrap(); + rcpt.insert(core.recipients.get("_").unwrap()) + } + }; }; if rcpt.is_empty() { - rcpt.insert(core.default); core.debug("No recipient or envelope address.").await.unwrap(); + rcpt.insert(core.recipients.get("_").unwrap()); }; // prepating message header let mut reply: Vec> = vec![]; if let Some(subject) = mail.subject() { @@ -164,11 +169,10 @@ core.sendfile(chat, obj).await.unwrap(); } } }, None => { core.debug("None mail.").await.unwrap(); }, - //send_to_sendgrid(mail, sendgrid_api_key).await; }; }); std::fs::remove_file(file.path())?; } @@ -178,48 +182,51 @@ fn my_prudence() -> Prudence { Prudence::default().with_read_timeout(Duration::from_secs(60)).with_banner_delay(Duration::from_secs(1)) } pub struct TelegramTransport { - default: UserId, tg: Api, recipients: HashMap, } impl TelegramTransport { - pub fn new(settings: &config::Config) -> TelegramTransport { - let api_key = settings.get_string("api_key").unwrap(); - let tg = Api::new(api_key); - let default_recipient = settings.get_string("default").unwrap(); - let recipients: HashMap = settings.get_table("recipients").unwrap().into_iter().map(|(a, b)| (a, UserId::new(b.into_int().unwrap()))).collect(); - // Barf if no default - let default = recipients[&default_recipient]; + pub fn new(settings: config::Config) -> TelegramTransport { + let tg = Api::new(settings.get_string("api_key") + .expect("[smtp2tg.toml] missing \"api_key\" parameter.\n")); + let recipients: HashMap = settings.get_table("recipients") + .expect("[smtp2tg.toml] missing table \"recipients\".\n") + .into_iter().map(|(a, b)| (a, UserId::new(b.into_int() + .expect("[smtp2tg.toml] \"recipient\" table values should be integers.\n") + ))).collect(); + if !recipients.contains_key("_") { + eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n"); + panic!("no default recipient"); + } TelegramTransport { - default, tg, recipients, } } pub async fn debug<'b, S>(&self, msg: S) -> Result<()> where S: Into> { task::sleep(Duration::from_secs(5)).await; - self.tg.send(SendMessage::new(self.default, msg) + self.tg.send(SendMessage::new(self.recipients.get("_").unwrap(), msg) .parse_mode(ParseMode::Markdown)).await?; Ok(()) } - pub async fn send<'b, S>(&self, to: UserId, msg: S) -> Result<()> + pub async fn send<'b, S>(&self, to: &UserId, msg: S) -> Result<()> where S: Into> { task::sleep(Duration::from_secs(5)).await; self.tg.send(SendMessage::new(to, msg) .parse_mode(ParseMode::Markdown)).await?; Ok(()) } - pub async fn sendfile(&self, to: UserId, chunk: V) -> Result<()> + pub async fn sendfile(&self, to: &UserId, chunk: V) -> Result<()> where V: Into { task::sleep(Duration::from_secs(5)).await; self.tg.send(telegram_bot::SendDocument::new(to, chunk)).await?; Ok(()) } @@ -227,15 +234,19 @@ #[async_std::main] async fn main() { let settings: config::Config = config::Config::builder() .add_source(config::File::with_name("smtp2tg.toml")) - .build().unwrap(); + .build() + .expect("[smtp2tg.toml] there was an error reading config\n\ + \tplease consult \"smtp2tg.toml.example\" for details"); - let core = TelegramTransport::new(&settings); - let maildir: PathBuf = settings.get_string("maildir").unwrap().into(); - let listen_on = settings.get_string("listen_on").unwrap(); + let maildir: PathBuf = settings.get_string("maildir") + .expect("[smtp2tg.toml] missing \"maildir\" parameter.\n").into(); + let listen_on = settings.get_string("listen_on") + .expect("[smtp2tg.toml] missing \"listen_on\" parameter.\n"); + let core = TelegramTransport::new(settings); let sink = Builder + Name::new("smtp2tg") + DebugService + my_prudence() + MailDir::new(maildir.clone()).unwrap(); task::spawn(async move { loop { @@ -244,16 +255,45 @@ } }); match listen_on.as_str() { "socket" => { + let socket_path = "./smtp2tg.sock"; + match std::fs::symlink_metadata(socket_path) { + Ok(metadata) => { + if metadata.file_type().is_socket() { + std::fs::remove_file(socket_path) + .expect("[smtp2tg] failed to remove old socket.\n"); + } else { + eprintln!("[smtp2tg] \"./smtp2tg.sock\" we wanted to use is actually not a socket.\n\ + [smtp2tg] please check the file and remove it manually.\n"); + panic!("socket path unavailable"); + } + }, + Err(err) => { + match err.kind() { + std::io::ErrorKind::NotFound => {}, + _ => { + eprintln!("{:?}", err); + panic!("unhandled file type error"); + } + }; + } + }; + let sink = sink + samotop::smtp::Lmtp.with(SmtpParser); - samotop::server::UnixServer::on("./smtp2tg.sock") + task::spawn(async move { + // Postpone mode change on the socket. I can't actually change + // other way, as UnixServer just grabs path, and blocks + task::sleep(Duration::from_secs(1)).await; + std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o777)).unwrap(); + }); + samotop::server::UnixServer::on(socket_path) .serve(sink.build()).await.unwrap(); }, _ => { let sink = sink + samotop::smtp::Esmtp.with(SmtpParser); samotop::server::TcpServer::on(listen_on) .serve(sink.build()).await.unwrap(); }, }; }