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