Annotation For src/main.rs
Logged in as anonymous

Lines of src/main.rs from check-in e81897ec87 that are changed by the sequence of edits moving toward check-in f4cad2a5c0:

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