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