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