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