Lines of
src/mail.rs
from check-in 0f47e23e21
that are changed by the sequence of edits moving toward
check-in c996f5c871:
1: use crate::{
2: Cursor,
3: telegram::TelegramTransport,
4: utils::{
5: Attachment,
6: RE_DOMAIN,
7: validate,
8: },
9: };
10:
11: use std::{
0f47e23e21 2026-01-12 12: borrow::Cow,
13: collections::{
14: HashMap,
15: HashSet,
16: },
17: io::Error,
18: sync::Arc,
19: };
20:
21: use async_compat::Compat;
22: use mailin_embedded::{
23: Response,
24: response::{
25: INTERNAL_ERROR,
26: INVALID_CREDENTIALS,
27: NO_MAILBOX,
28: OK
29: },
30: };
31: use regex::{
32: Regex,
33: escape,
34: };
35: use stacked_errors::{
36: Result,
37: StackableErr,
38: bail,
39: };
40: use tgbot::types::ChatPeerId;
41:
42: /// `SomeHeaders` object to store data through SMTP session
43: #[derive(Clone, Debug)]
44: struct SomeHeaders {
45: from: String,
46: to: Vec<String>,
47: }
48:
49: /// `MailServer` Central object with TG api and configuration
50: #[derive(Clone, Debug)]
51: pub struct MailServer {
52: data: Vec<u8>,
53: headers: Option<SomeHeaders>,
54: relay: bool,
55: tg: Arc<TelegramTransport>,
56: fields: HashSet<String>,
57: address: Regex,
58: }
59:
60: impl MailServer {
61: /// Initialize API and read configuration
62: pub fn new(settings: config::Config) -> Result<MailServer> {
63: let api_key = settings.get_string("api_key")
64: .context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?;
65: let mut recipients = HashMap::new();
66: for (name, value) in settings.get_table("recipients")
67: .expect("[smtp2tg.toml] missing table \"recipients\".\n")
68: {
69: let value = value.into_int()
70: .context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
71: recipients.insert(name, value);
72: }
73:
74: let tg = Arc::new(TelegramTransport::new(api_key, recipients, &settings)?);
75: let fields = HashSet::<String>::from_iter(settings.get_array("fields")
76: .expect("[smtp2tg.toml] \"fields\" should be an array")
77: .iter().map(|x| x.clone().into_string().expect("should be strings")));
78: let mut domains: HashSet<String> = HashSet::new();
79: let extra_domains = settings.get_array("domains").stack()?;
80: for domain in extra_domains {
81: let domain = domain.to_string().to_lowercase();
82: if RE_DOMAIN.is_match(&domain) {
83: domains.insert(domain);
84: } else {
85: panic!("[smtp2tg.toml] can't check of domains in \"domains\": {domain}");
86: }
87: }
88: let domains = domains.into_iter().map(|s| escape(&s))
89: .collect::<Vec<String>>().join("|");
90: let address = Regex::new(&format!("^(?P<user>[a-z0-9][-a-z0-9])(@({domains}))$")).stack()?;
91: let relay = match settings.get_string("unknown")
92: .context("[smtp2tg.toml] can't get \"unknown\" policy.\n")?.as_str()
93: {
94: "relay" => true,
95: "deny" => false,
96: _ => {
97: bail!("[smtp2tg.toml] \"unknown\" should be either \"relay\" or \"deny\".\n");
98: },
99: };
100:
101: Ok(MailServer {
102: data: vec!(),
103: headers: None,
104: relay,
105: tg,
106: fields,
107: address,
108: })
109: }
110:
111: /// Returns id for provided email address
112: fn get_id (&self, name: &str) -> Result<&ChatPeerId> {
113: // here we need to store String locally to borrow it after
114: let mut link = name;
115: let name: String;
116: if let Some(caps) = self.address.captures(link) {
117: name = caps["name"].to_string();
118: link = &name;
119: }
120: match self.tg.get(link) {
121: Ok(addr) => Ok(addr),
122: Err(_) => Ok(&self.tg.default),
123: }
124: }
125:
126: /// Attempt to deliver one message
127: async fn relay_mail (&self) -> Result<()> {
128: if let Some(headers) = &self.headers {
129: let mail = mail_parser::MessageParser::new().parse(&self.data)
130: .context("Failed to parse mail.")?;
131:
132: // Adding all known addresses to recipient list, for anyone else adding default
133: // Also if list is empty also adding default
134: let mut rcpt: HashSet<&ChatPeerId> = HashSet::new();
135: if headers.to.is_empty() && !self.relay {
136: bail!("Relaying is disabled, and there's no destination address");
137: }
138: for item in &headers.to {
139: rcpt.insert(self.get_id(item)?);
140: };
141: if rcpt.is_empty() {
142: self.tg.debug("No recipient or envelope address.").await?;
143: rcpt.insert(&self.tg.default);
144: };
145:
146: // preparing message header
147: let mut reply: Vec<String> = vec!["<blockquote expandable>".into()];
148: if self.fields.contains("subject") {
149: if let Some(subject) = mail.subject() {
150: reply.push(format!("<u><i>Subject:</i></u> <code>{}</code>", validate(subject).stack()?));
151: } else if let Some(thread) = mail.thread_name() {
152: reply.push(format!("<u><i>Thread:</i></u> <code>{}</code>", validate(thread).stack()?));
153: }
154: }
155: // do we need to replace spaces here?
156: if self.fields.contains("from") {
157: reply.push(format!("<u><i>From:</i></u> <code>{}</code>", validate(&headers.from).stack()?));
158: }
159: if self.fields.contains("date")
160: && let Some(date) = mail.date()
161: {
162: reply.push(format!("<u><i>Date:</i></u> <code>{date}</code>"));
163: }
164: reply.push("</blockquote><pre>".into());
0f47e23e21 2026-01-12 165: //let header_size = reply.join(" ").len();
0f47e23e21 2026-01-12 166: let mut header_size = 0;
0f47e23e21 2026-01-12 167: for i in reply.iter() {
0f47e23e21 2026-01-12 168: header_size += i.len() + 1;
0f47e23e21 2026-01-12 169: }
170:
171: let html_parts = mail.html_body_count();
172: let text_parts = mail.text_body_count();
173: let attachments = mail.attachment_count();
174: if html_parts != text_parts {
175: self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
176: }
177: //let mut html_num = 0;
178: let mut text_num = 0;
179: let mut file_num = 0;
180: // let's display first html or text part as body
0f47e23e21 2026-01-12 181: let mut body: Cow<'_, str> = "".into();
182: /*
183: * actually I don't wanna parse that html stuff
184: if html_parts > 0 {
185: let text = mail.body_html(0).stack()?;
186: if text.len() < 4096 - header_size {
187: body = text;
188: html_num = 1;
189: }
190: };
191: */
192: if body.is_empty() && text_parts > 0 {
193: let text = mail.body_text(0)
0f47e23e21 2026-01-12 194: .context("Failed to extract text from message")?;
0f47e23e21 2026-01-12 195: // 7:
196: // - (mail text)
0f47e23e21 2026-01-12 197: // - 1 trailing newline
198: // - 6: </pre>
0f47e23e21 2026-01-12 199: if text.len() < 4096 - ( header_size + 7 ) {
200: body = text;
201: text_num = 1;
202: }
203: };
0f47e23e21 2026-01-12 204: reply.extend(body.lines().map(|x| x.into()));
0f47e23e21 2026-01-12 205: reply.push("</pre>".into());
206:
207: // and let's collect all other attachment parts
208: let mut files_to_send = vec![];
209: /*
210: * let's just skip html parts for now, they just duplicate text?
211: while html_num < html_parts {
212: files_to_send.push(mail.html_part(html_num).stack()?);
213: html_num += 1;
214: }
215: */
216: while text_num < text_parts {
217: files_to_send.push(mail.text_part(text_num.try_into().stack()?)
218: .context("Failed to get text part from message.")?);
219: text_num += 1;
220: }
221: while file_num < attachments {
222: files_to_send.push(mail.attachment(file_num.try_into().stack()?)
223: .context("Failed to get file part from message.")?);
224: file_num += 1;
225: }
226:
0f47e23e21 2026-01-12 227: let msg = reply.join("\n");
228: for chat in rcpt {
229: if !files_to_send.is_empty() {
230: let mut files = vec![];
231: // let mut first_one = true;
232: for chunk in &files_to_send {
233: let data: Vec<u8> = chunk.contents().to_vec();
234: let mut filename: Option<String> = None;
235: for header in chunk.headers() {
236: if header.name() == "Content-Type" {
237: match header.value() {
238: mail_parser::HeaderValue::ContentType(contenttype) => {
239: if let Some(fname) = contenttype.attribute("name") {
240: filename = Some(fname.to_owned());
241: }
242: },
243: _ => {
244: self.tg.debug("Attachment has bad ContentType header.").await?;
245: },
246: };
247: };
248: };
249: let filename = if let Some(fname) = filename {
250: fname
251: } else {
252: "Attachment.txt".into()
253: };
254: files.push(Attachment {
255: data: Cursor::new(data),
256: name: filename,
257: });
258: }
259: self.tg.sendgroup(chat, files, &msg).await?;
260: } else {
261: self.tg.send(chat, &msg).await?;
262: }
263: }
264: } else {
265: bail!("Required headers were not found.");
266: }
267: Ok(())
268: }
269: }
270:
271: impl mailin_embedded::Handler for MailServer {
272: /// Just deny login auth
273: fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
274: INVALID_CREDENTIALS
275: }
276:
277: /// Just deny plain auth
278: fn auth_plain (&mut self, _authorization_id: &str, _authentication_id: &str, _password: &str) -> Response {
279: INVALID_CREDENTIALS
280: }
281:
282: /// Verify whether address is deliverable
283: fn rcpt (&mut self, to: &str) -> Response {
284: if self.relay {
285: OK
286: } else {
287: match self.get_id(to) {
288: Ok(_) => OK,
289: Err(_) => {
290: if self.relay {
291: OK
292: } else {
293: NO_MAILBOX
294: }
295: }
296: }
297: }
298: }
299:
300: /// Save headers we need
301: fn data_start (&mut self, _domain: &str, from: &str, _is8bit: bool, to: &[String]) -> Response {
302: self.headers = Some(SomeHeaders{
303: from: from.to_string(),
304: to: to.to_vec(),
305: });
306: OK
307: }
308:
309: /// Save chunk(?) of data
310: fn data (&mut self, buf: &[u8]) -> std::result::Result<(), Error> {
311: self.data.append(buf.to_vec().as_mut());
312: Ok(())
313: }
314:
315: /// Attempt to send email, return temporary error if that fails
316: fn data_end (&mut self) -> Response {
317: let mut result = OK;
318: smol::block_on(Compat::new(async {
319: // relay mail
320: if let Err(err) = self.relay_mail().await {
321: result = INTERNAL_ERROR;
322: // in case that fails - inform default recipient
323: if let Err(err) = self.tg.debug(&format!("Sending emails failed:\n{err:?}")).await {
324: // in case that also fails - write some logs and bail
325: eprintln!("{err:?}");
326: };
327: };
328: }));
329: // clear - just in case
330: self.data = vec![];
331: self.headers = None;
332: result
333: }
334: }