Check-in [42ac4c7a9a]
Logged in as anonymous
Overview
Comment:simplify
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 42ac4c7a9a14e744479784146292ffe90d7c39cdaec4d5b5486f8a6100c93421
User & Date: arcade on 2026-10-28 17:22:21.130
Other Links: manifest | tags
Context
2026-10-28
17:33
case insensitive check and some tests leaf check-in: e3cad77b2b user: arcade tags: trunk
17:22
simplify check-in: 42ac4c7a9a user: arcade tags: trunk
17:13
bumps, global permissions for workflow, docs check-in: ec55d3e69e user: arcade tags: trunk
Changes
︙
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198


199
200
201

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

220
221
222
223
224
225
226
227
228
229

230
231
232
233
234
235

236
237
238
239
240

241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264














265
266
267
268
269
270
271
272
273

274
275
276
277
278
279
280
183
184
185
186
187
188
189

190
191
192

193



194
195



196








197
198
199
200
201
202
203
204
205

206
207
208
209
210
211





212






213





214



215
216
217
218

219
220
221













222
223
224
225
226
227
228
229
230
231
232
233
234
235






236
237

238
239
240
241
242
243
244
245







-



-

-
-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
-









-
+





-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-




-



-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-


-
+







				reply.push(format!("<u><i>Date:</i></u> <code>{date}</code>"));
			}
			reply.push("</blockquote><pre>".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();
			// Display the first text part as the body if it fits.
			let mut body = String::new();
			/*
			 * actually I don't wanna parse that html stuff
			if html_parts > 0 {
			if text_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 {
				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: </pre>
				if text.len() < 4096 - ( reply.len() + 6 ) {
					body = text.to_string();
					body = text.into_owned();
					text_num = 1;
				}
			};
			let msg = format!("{}{}</pre>", 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 {
			// Send remaining text-body parts followed by attachments.
				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)
			let files_to_send: Vec<_> = mail.text_body().skip(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)
				.chain(mail.attachments()).collect();
					.context("Failed to get file part from message.")?);
				file_num += 1;
			}

			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<u8> = chunk.decoded().to_vec();
						let mut filename: Option<String> = 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?;
									},
								};
							};
						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?;
								},
							};
						};
						};
						let filename = if let Some(fname) = filename {
							fname
						} else {
							"Attachment.txt".into()
						};
						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?;
				}
			}
︙
︙
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
9
10
11
12
13
14
15

16


17
18
19
20
21
22
23







-
+
-
-








use std::{
	os::unix::fs::PermissionsExt,
	path::Path,
};

use clap::Parser;
use smol::{
use smol::fs::metadata;
	fs::metadata,
};
use stacked_errors::{
	Result,
	StackableErr,
	bail,
};

use smtp2tg::mail::MailServer;
︙
47
48
49
50
51
52
53


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82






























83
84
85
86
87
88
89
45
46
47
48
49
50
51
52
53





























54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90







+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







/// Also returns an error if the system hostname cannot be read or is not
/// valid UTF-8, or if mail server initialisation fails.
///
/// # Panics
/// Panics if configuring or serving the SMTP server fails.
	async fn main (ex: &LocalExecutor<'_>) -> Result<()> {
		let args = Args::parse();
		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());
		}	}
		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 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 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);

		// TODO: remove unwraps when mailin-embedded bumps with better error handling
︙