Lines of src/main.rs from check-in 31aec3c4b0 that are changed by the sequence of edits moving toward check-in e81897ec87:
31aec3c4b0 2024-05-23 1: use anyhow::Result; 2: use async_std::task; 3: use samotop::{ 4: mail::{ 5: Builder, 6: DebugService, 7: MailDir, 8: Name 9: }, 10: smtp::{ 11: SmtpParser, 12: Prudence, 13: }, 14: }; 15: use telegram_bot::{ 16: Api, 17: MessageOrChannelPost, 18: ParseMode, 19: SendMessage, 20: UserId, 21: }; 22: 23: use std::{ 24: borrow::Cow, 25: collections::{ 26: HashMap, 27: HashSet, 28: }, 29: io::Read, 30: os::unix::fs::{ 31: FileTypeExt, 32: PermissionsExt, 33: }, 34: path::{ 35: Path, 36: PathBuf 37: }, 38: time::Duration, 39: vec::Vec, 40: }; 41: 42: fn address_into_iter<'a>(addr: &'a mail_parser::Address<'a, >) -> impl Iterator<Item = Cow<'a, str>> { 43: addr.clone().into_list().into_iter().map(|a| a.address.unwrap()) 44: } 45: 31aec3c4b0 2024-05-23 46: fn relay_mails(maildir: &Path, core: &TelegramTransport) -> Result<()> { 47: let new_dir = maildir.join("new"); 48: 49: std::fs::create_dir_all(&new_dir)?; 50: 51: let files = std::fs::read_dir(new_dir)?; 52: for file in files { 53: let file = file?; 54: let mut buf = Vec::new(); 55: std::fs::File::open(file.path())?.read_to_end(&mut buf)?; 56: 31aec3c4b0 2024-05-23 57: task::block_on(async move { 31aec3c4b0 2024-05-23 58: match mail_parser::MessageParser::default().parse(&buf[..]) { 31aec3c4b0 2024-05-23 59: Some(mail) => { 31aec3c4b0 2024-05-23 60: let mail = mail.clone(); 31aec3c4b0 2024-05-23 61: 31aec3c4b0 2024-05-23 62: // Fetching address lists from fields we know 31aec3c4b0 2024-05-23 63: let mut to = HashSet::new(); 31aec3c4b0 2024-05-23 64: if let Some(addr) = mail.to() { 31aec3c4b0 2024-05-23 65: let _ = address_into_iter(addr).map(|x| to.insert(x)); 31aec3c4b0 2024-05-23 66: }; 31aec3c4b0 2024-05-23 67: if let Some(addr) = mail.header("X-Samotop-To") { 31aec3c4b0 2024-05-23 68: match addr { 31aec3c4b0 2024-05-23 69: mail_parser::HeaderValue::Address(addr) => { 31aec3c4b0 2024-05-23 70: let _ = address_into_iter(addr).map(|x| to.insert(x)); 31aec3c4b0 2024-05-23 71: }, 31aec3c4b0 2024-05-23 72: mail_parser::HeaderValue::Text(text) => { 31aec3c4b0 2024-05-23 73: to.insert(text.clone()); 31aec3c4b0 2024-05-23 74: }, 31aec3c4b0 2024-05-23 75: _ => {} 31aec3c4b0 2024-05-23 76: } 31aec3c4b0 2024-05-23 77: }; 31aec3c4b0 2024-05-23 78: 31aec3c4b0 2024-05-23 79: // Adding all known addresses to recipient list, for anyone else adding default 31aec3c4b0 2024-05-23 80: // Also if list is empty also adding default 31aec3c4b0 2024-05-23 81: let mut rcpt: HashSet<&UserId> = HashSet::new(); 31aec3c4b0 2024-05-23 82: for item in to { 31aec3c4b0 2024-05-23 83: let item = item.into_owned(); 31aec3c4b0 2024-05-23 84: match core.recipients.get(&item) { 31aec3c4b0 2024-05-23 85: Some(addr) => rcpt.insert(addr), 31aec3c4b0 2024-05-23 86: None => { 31aec3c4b0 2024-05-23 87: core.debug(format!("Recipient [{}] not found.", &item)).await.unwrap(); 31aec3c4b0 2024-05-23 88: rcpt.insert(core.recipients.get("_").unwrap()) 31aec3c4b0 2024-05-23 89: } 31aec3c4b0 2024-05-23 90: }; 31aec3c4b0 2024-05-23 91: }; 31aec3c4b0 2024-05-23 92: if rcpt.is_empty() { 31aec3c4b0 2024-05-23 93: core.debug("No recipient or envelope address.").await.unwrap(); 31aec3c4b0 2024-05-23 94: rcpt.insert(core.recipients.get("_").unwrap()); 31aec3c4b0 2024-05-23 95: }; 31aec3c4b0 2024-05-23 96: 31aec3c4b0 2024-05-23 97: // prepating message header 31aec3c4b0 2024-05-23 98: let mut reply: Vec<Cow<str>> = vec![]; 31aec3c4b0 2024-05-23 99: if let Some(subject) = mail.subject() { 31aec3c4b0 2024-05-23 100: reply.push(format!("**Subject:** `{}`", subject).into()); 31aec3c4b0 2024-05-23 101: } else if let Some(thread) = mail.thread_name() { 31aec3c4b0 2024-05-23 102: reply.push(format!("**Thread:** `{}`", thread).into()); 31aec3c4b0 2024-05-23 103: } 31aec3c4b0 2024-05-23 104: if let Some(from) = mail.from() { 31aec3c4b0 2024-05-23 105: reply.push(format!("**From:** `{:?}`", address_into_iter(from).collect::<Vec<_>>().join(", ")).into()); 31aec3c4b0 2024-05-23 106: } 31aec3c4b0 2024-05-23 107: if let Some(sender) = mail.sender() { 31aec3c4b0 2024-05-23 108: reply.push(format!("**Sender:** `{:?}`", address_into_iter(sender).collect::<Vec<_>>().join(", ")).into()); 31aec3c4b0 2024-05-23 109: } 31aec3c4b0 2024-05-23 110: reply.push("".into()); 31aec3c4b0 2024-05-23 111: let header_size = reply.join("\n").len() + 1; 31aec3c4b0 2024-05-23 112: 31aec3c4b0 2024-05-23 113: let html_parts = mail.html_body_count(); 31aec3c4b0 2024-05-23 114: let text_parts = mail.text_body_count(); 31aec3c4b0 2024-05-23 115: let attachments = mail.attachment_count(); 31aec3c4b0 2024-05-23 116: if html_parts != text_parts { 31aec3c4b0 2024-05-23 117: core.debug(format!("Hm, we have {} HTML parts and {} text parts.", html_parts, text_parts)).await.unwrap(); 31aec3c4b0 2024-05-23 118: } 31aec3c4b0 2024-05-23 119: //let mut html_num = 0; 31aec3c4b0 2024-05-23 120: let mut text_num = 0; 31aec3c4b0 2024-05-23 121: let mut file_num = 0; 31aec3c4b0 2024-05-23 122: // let's display first html or text part as body 31aec3c4b0 2024-05-23 123: let mut body = "".into(); 31aec3c4b0 2024-05-23 124: /* 31aec3c4b0 2024-05-23 125: * actually I don't wanna parse that html stuff 31aec3c4b0 2024-05-23 126: if html_parts > 0 { 31aec3c4b0 2024-05-23 127: let text = mail.body_html(0).unwrap(); 31aec3c4b0 2024-05-23 128: if text.len() < 4096 - header_size { 31aec3c4b0 2024-05-23 129: body = text; 31aec3c4b0 2024-05-23 130: html_num = 1; 31aec3c4b0 2024-05-23 131: } 31aec3c4b0 2024-05-23 132: }; 31aec3c4b0 2024-05-23 133: */ 31aec3c4b0 2024-05-23 134: if body == "" && text_parts > 0 { 31aec3c4b0 2024-05-23 135: let text = mail.body_text(0).unwrap(); 31aec3c4b0 2024-05-23 136: if text.len() < 4096 - header_size { 31aec3c4b0 2024-05-23 137: body = text; 31aec3c4b0 2024-05-23 138: text_num = 1; 31aec3c4b0 2024-05-23 139: } 31aec3c4b0 2024-05-23 140: }; 31aec3c4b0 2024-05-23 141: reply.push("```".into()); 31aec3c4b0 2024-05-23 142: for line in body.lines() { 31aec3c4b0 2024-05-23 143: reply.push(line.into()); 31aec3c4b0 2024-05-23 144: } 31aec3c4b0 2024-05-23 145: reply.push("```".into()); 31aec3c4b0 2024-05-23 146: 31aec3c4b0 2024-05-23 147: // and let's collect all other attachment parts 31aec3c4b0 2024-05-23 148: let mut files_to_send = vec![]; 31aec3c4b0 2024-05-23 149: /* 31aec3c4b0 2024-05-23 150: * let's just skip html parts for now, they just duplicate text? 31aec3c4b0 2024-05-23 151: while html_num < html_parts { 31aec3c4b0 2024-05-23 152: files_to_send.push(mail.html_part(html_num).unwrap()); 31aec3c4b0 2024-05-23 153: html_num += 1; 31aec3c4b0 2024-05-23 154: } 31aec3c4b0 2024-05-23 155: */ 31aec3c4b0 2024-05-23 156: while text_num < text_parts { 31aec3c4b0 2024-05-23 157: files_to_send.push(mail.text_part(text_num).unwrap()); 31aec3c4b0 2024-05-23 158: text_num += 1; 31aec3c4b0 2024-05-23 159: } 31aec3c4b0 2024-05-23 160: while file_num < attachments { 31aec3c4b0 2024-05-23 161: files_to_send.push(mail.attachment(file_num).unwrap()); 31aec3c4b0 2024-05-23 162: file_num += 1; 31aec3c4b0 2024-05-23 163: } 31aec3c4b0 2024-05-23 164: 31aec3c4b0 2024-05-23 165: for chat in rcpt { 31aec3c4b0 2024-05-23 166: let base_post = core.send(chat, reply.join("\n")).await.unwrap(); 31aec3c4b0 2024-05-23 167: for chunk in &files_to_send { 31aec3c4b0 2024-05-23 168: let data = chunk.contents().to_vec(); 31aec3c4b0 2024-05-23 169: let obj = telegram_bot::types::InputFileUpload::with_data(data, "Attachment"); 31aec3c4b0 2024-05-23 170: core.sendfile(chat, obj, Some(&base_post)).await.unwrap(); 31aec3c4b0 2024-05-23 171: } 31aec3c4b0 2024-05-23 172: } 31aec3c4b0 2024-05-23 173: }, 31aec3c4b0 2024-05-23 174: None => { core.debug("None mail.").await.unwrap(); }, 31aec3c4b0 2024-05-23 175: }; 31aec3c4b0 2024-05-23 176: }); 177: 178: std::fs::remove_file(file.path())?; 179: } 180: Ok(()) 181: } 182: 183: fn my_prudence() -> Prudence { 184: Prudence::default().with_read_timeout(Duration::from_secs(60)).with_banner_delay(Duration::from_secs(1)) 185: } 186: 187: pub struct TelegramTransport { 188: tg: Api, 189: recipients: HashMap<String, UserId>, 190: } 191: 192: impl TelegramTransport { 193: pub fn new(settings: config::Config) -> TelegramTransport { 194: let tg = Api::new(settings.get_string("api_key") 195: .expect("[smtp2tg.toml] missing \"api_key\" parameter.\n")); 196: let recipients: HashMap<String, UserId> = settings.get_table("recipients") 197: .expect("[smtp2tg.toml] missing table \"recipients\".\n") 198: .into_iter().map(|(a, b)| (a, UserId::new(b.into_int() 199: .expect("[smtp2tg.toml] \"recipient\" table values should be integers.\n") 200: ))).collect(); 201: if !recipients.contains_key("_") { 202: eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n"); 203: panic!("no default recipient"); 204: } 205: 206: TelegramTransport { 207: tg, 208: recipients, 209: } 210: } 211: 212: pub async fn debug<'b, S>(&self, msg: S) -> Result<MessageOrChannelPost> 213: where S: Into<Cow<'b, str>> { 214: task::sleep(Duration::from_secs(5)).await; 215: Ok(self.tg.send(SendMessage::new(self.recipients.get("_").unwrap(), msg) 216: .parse_mode(ParseMode::Markdown)).await?) 217: } 218: 219: pub async fn send<'b, S>(&self, to: &UserId, msg: S) -> Result<MessageOrChannelPost> 220: where S: Into<Cow<'b, str>> { 221: task::sleep(Duration::from_secs(5)).await; 222: Ok(self.tg.send(SendMessage::new(to, msg) 223: .parse_mode(ParseMode::Markdown)).await?) 224: } 225: 226: pub async fn sendfile<V>(&self, to: &UserId, chunk: V, basic_mail: Option<&MessageOrChannelPost>) -> Result<()> 227: where V: Into<telegram_bot::InputFile> { 228: task::sleep(Duration::from_secs(5)).await; 229: match basic_mail { 230: Some(post) => { 231: self.tg.send(telegram_bot::SendDocument::new(to, chunk).reply_to(post)).await?; 232: }, 233: None => { 234: self.tg.send(telegram_bot::SendDocument::new(to, chunk)).await?; 235: }, 236: }; 237: Ok(()) 238: } 239: } 240: 241: #[async_std::main] 242: async fn main() { 243: let settings: config::Config = config::Config::builder() 244: .add_source(config::File::with_name("smtp2tg.toml")) 245: .build() 246: .expect("[smtp2tg.toml] there was an error reading config\n\ 247: \tplease consult \"smtp2tg.toml.example\" for details"); 248: 249: let maildir: PathBuf = settings.get_string("maildir") 250: .expect("[smtp2tg.toml] missing \"maildir\" parameter.\n").into(); 251: let listen_on = settings.get_string("listen_on") 252: .expect("[smtp2tg.toml] missing \"listen_on\" parameter.\n"); 253: let core = TelegramTransport::new(settings); 254: let sink = Builder + Name::new("smtp2tg") + DebugService + 255: my_prudence() + MailDir::new(maildir.clone()).unwrap(); 256: 257: env_logger::init(); 258: 259: task::spawn(async move { 260: loop { 31aec3c4b0 2024-05-23 261: relay_mails(&maildir, &core).unwrap(); 262: task::sleep(Duration::from_secs(5)).await; 263: } 264: }); 265: 266: match listen_on.as_str() { 267: "socket" => { 268: let socket_path = "./smtp2tg.sock"; 269: match std::fs::symlink_metadata(socket_path) { 270: Ok(metadata) => { 271: if metadata.file_type().is_socket() { 272: std::fs::remove_file(socket_path) 273: .expect("[smtp2tg] failed to remove old socket.\n"); 274: } else { 275: eprintln!("[smtp2tg] \"./smtp2tg.sock\" we wanted to use is actually not a socket.\n\ 276: [smtp2tg] please check the file and remove it manually.\n"); 277: panic!("socket path unavailable"); 278: } 279: }, 280: Err(err) => { 281: match err.kind() { 282: std::io::ErrorKind::NotFound => {}, 283: _ => { 284: eprintln!("{:?}", err); 285: panic!("unhandled file type error"); 286: } 287: }; 288: } 289: }; 290: 291: let sink = sink + samotop::smtp::Lmtp.with(SmtpParser); 292: task::spawn(async move { 293: // Postpone mode change on the socket. I can't actually change 294: // other way, as UnixServer just grabs path, and blocks 295: task::sleep(Duration::from_secs(1)).await; 296: std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o777)).unwrap(); 297: }); 298: samotop::server::UnixServer::on(socket_path) 299: .serve(sink.build()).await.unwrap(); 300: }, 301: _ => { 302: let sink = sink + samotop::smtp::Esmtp.with(SmtpParser); 303: samotop::server::TcpServer::on(listen_on) 304: .serve(sink.build()).await.unwrap(); 305: }, 306: }; 307: }