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