Annotation For src/main.rs
Logged in as anonymous

Lines of src/main.rs from check-in 866aad57a4 that are changed by the sequence of edits moving toward check-in 1db9dbe390:

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