Annotation For src/main.rs
Logged in as anonymous

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: }