Index: src/mail.rs ================================================================== --- src/mail.rs +++ src/mail.rs @@ -185,94 +185,59 @@ reply.push("
".into());
 			let reply = reply.join("\n");
 
 			let text_parts = mail.text_body().len();
 			let html_parts = mail.html_body().len();
-			let attachments = mail.attachments().len();
 			if html_parts != text_parts {
 				self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
 			}
-			//let mut html_num = 0;
 			let mut text_num = 0;
-			let mut file_num = 0;
-			// let's display first html or text part as body
-			let mut body: String = "".into();
-			/*
-			 * actually I don't wanna parse that html stuff
-			if html_parts > 0 {
-				let text = mail.body_html(0).stack()?;
-				if text.len() < 4096 - header_size {
-					body = text;
-					html_num = 1;
-				}
-			};
-			*/
-			if body.is_empty() && text_parts > 0 {
+			// Display the first text part as the body if it fits.
+			let mut body = String::new();
+			if text_parts > 0 {
 				let text = mail.body_text(0)
 					.context("Failed to extract text from message")?
 					.replace("\r\n", "\n");
 				let text = validate(&text).stack()?;
 				// 6:
 				// - (headers)
 				// - (mail text)
 				// - 6: 
if text.len() < 4096 - ( reply.len() + 6 ) { - body = text.to_string(); + body = text.into_owned(); text_num = 1; } }; let msg = format!("{}{}", reply, body); - // and let's collect all other attachment parts - let mut files_to_send = vec![]; - /* - * let's just skip html parts for now, they just duplicate text? - while html_num < html_parts { - files_to_send.push(mail.html_part(html_num).stack()?); - html_num += 1; - } - */ - while text_num < text_parts { - files_to_send.push(mail.text_body().nth(text_num) - .context("Failed to get text part from message.")?); - text_num += 1; - } - while file_num < attachments { - files_to_send.push(mail.attachments().nth(file_num) - .context("Failed to get file part from message.")?); - file_num += 1; - } + // Send remaining text-body parts followed by attachments. + let files_to_send: Vec<_> = mail.text_body().skip(text_num) + .chain(mail.attachments()).collect(); for chat in rcpt { if !files_to_send.is_empty() { let mut files = vec![]; - // let mut first_one = true; for chunk in &files_to_send { let data: Vec = chunk.decoded().to_vec(); let mut filename: Option = None; - for header in chunk.headers() { - if header.raw_name() == "Content-Type" { - match header.value() { - mail_parser::HeaderValue::ContentType(contenttype) => { - if let Some(fname) = contenttype.attribute("name") { - filename = Some(fname.to_owned()); - } - }, - _ => { - self.tg.debug("Attachment has bad ContentType header.").await?; - }, - }; - }; - }; - let filename = if let Some(fname) = filename { - fname - } else { - "Attachment.txt".into() + for header in chunk.headers().iter() + .filter(|header| header.raw_name() == "Content-Type") + { + match header.value() { + mail_parser::HeaderValue::ContentType(contenttype) => { + if let Some(fname) = contenttype.attribute("name") { + filename = Some(fname.to_owned()); + } + }, + _ => { + self.tg.debug("Attachment has bad ContentType header.").await?; + }, + }; }; files.push(Attachment { data: Cursor::new(data), - name: filename, + name: filename.unwrap_or_else(|| "Attachment.txt".into()), }); } self.tg.sendgroup(chat, files, &msg).await?; } else { self.tg.send(chat, &msg).await?; Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -11,13 +11,11 @@ os::unix::fs::PermissionsExt, path::Path, }; use clap::Parser; -use smol::{ - fs::metadata, -}; +use smol::fs::metadata; use stacked_errors::{ Result, StackableErr, bail, }; @@ -49,39 +47,42 @@ /// /// # Panics /// Panics if configuring or serving the SMTP server fails. async fn main (ex: &LocalExecutor<'_>) -> Result<()> { let args = Args::parse(); - let config_file = Path::new(&args.config); - if !config_file.try_exists().stack()? { - bail!("Configuration file not found: {config_file:?}\n\ - Hint: Ensure the file exists and the path is correct."); + let settings = { + // Load configuration defaults and overrides after checking file permissions. + let config_file = Path::new(&args.config); + if !config_file.try_exists().stack()? { + bail!("Configuration file not found: {config_file:?}\n\ + Hint: Ensure the file exists and the path is correct."); + }; + { + let meta = metadata(config_file).await.stack()?; + if (!0o100600 & meta.permissions().mode()) > 0 { + bail!("Configuration file permissions are insecure {config_file:?}\n\ + Current permissions: {:o}\n\ + Required: 0600 (owner read/write only).\n\ + Fix with: chmod 600 {config_file:?}", + meta.permissions().mode()); + } } + config::Config::builder() + .set_default("api_gateway", "https://api.telegram.org").stack()? + .set_default("fields", vec!["date", "from", "subject"]).stack()? + .set_default("hostname", "smtp.2.tg").stack()? + .set_default("listen_on", "0.0.0.0:1025").stack()? + .set_default("domains", vec!["localhost", + hostname::get().context("Failed to get current hostname")? + .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()? + .add_source(config::File::from(config_file)) + .build() + .with_context(|| format!( + "Failed to parse configuration file: {config_file:?}\n\ + Check syntax against smtp2tg.toml.example.\n\ + Common issues: missing quotes, trailing commas in inline tables, or invalid types." + ))? }; - { - let meta = metadata(config_file).await.stack()?; - if (!0o100600 & meta.permissions().mode()) > 0 { - bail!("Configuration file permissions are insecure {config_file:?}\n\ - Current permissions: {:o}\n\ - Required: 0600 (owner read/write only).\n\ - Fix with: chmod 600 {config_file:?}", - meta.permissions().mode()); - } } - let settings: config::Config = config::Config::builder() - .set_default("api_gateway", "https://api.telegram.org").stack()? - .set_default("fields", vec!["date", "from", "subject"]).stack()? - .set_default("hostname", "smtp.2.tg").stack()? - .set_default("listen_on", "0.0.0.0:1025").stack()? - .set_default("domains", vec!["localhost", - hostname::get().context("Failed to get current hostname")? - .to_str().context("Can't convert hostname to string, bad UTF-8?")?]).stack()? - .add_source(config::File::from(config_file)) - .build() - .with_context(|| format!( - "Failed to parse configuration file: {config_file:?}\n\ - Check syntax against smtp2tg.toml.example.\n\ - Common issues: missing quotes, trailing commas in inline tables, or invalid types." - ))?; let listen_on = settings.get_string("listen_on").stack()?; let server_name = settings.get_string("hostname").stack()?; let core = MailServer::new(settings)?; let mut server = mailin_embedded::Server::new(core);