e81897ec87 2024-05-24 1: use anyhow::{
e81897ec87 2024-05-24 2: anyhow,
e81897ec87 2024-05-24 3: Result,
e81897ec87 2024-05-24 4: };
7620f854a7 2024-05-21 5: use async_std::task;
7620f854a7 2024-05-21 6: use samotop::{
7620f854a7 2024-05-21 7: mail::{
7620f854a7 2024-05-21 8: Builder,
7620f854a7 2024-05-21 9: DebugService,
7620f854a7 2024-05-21 10: MailDir,
7620f854a7 2024-05-21 11: Name
7620f854a7 2024-05-21 12: },
51adce1e7e 2024-05-22 13: smtp::{
51adce1e7e 2024-05-22 14: SmtpParser,
51adce1e7e 2024-05-22 15: Prudence,
51adce1e7e 2024-05-22 16: },
7620f854a7 2024-05-21 17: };
f4cad2a5c0 2024-05-26 18: use teloxide::{
f4cad2a5c0 2024-05-26 19: Bot,
f4cad2a5c0 2024-05-26 20: prelude::{
f4cad2a5c0 2024-05-26 21: Requester,
f4cad2a5c0 2024-05-26 22: RequesterExt,
f4cad2a5c0 2024-05-26 23: },
f4cad2a5c0 2024-05-26 24: types::{
f4cad2a5c0 2024-05-26 25: ChatId,
f4cad2a5c0 2024-05-26 26: ParseMode::MarkdownV2,
f4cad2a5c0 2024-05-26 27: },
7620f854a7 2024-05-21 28: };
7620f854a7 2024-05-21 29:
7620f854a7 2024-05-21 30: use std::{
7620f854a7 2024-05-21 31: borrow::Cow,
61238a3618 2024-05-22 32: collections::{
61238a3618 2024-05-22 33: HashMap,
61238a3618 2024-05-22 34: HashSet,
61238a3618 2024-05-22 35: },
7620f854a7 2024-05-21 36: io::Read,
da7fc7983d 2024-05-23 37: os::unix::fs::{
da7fc7983d 2024-05-23 38: FileTypeExt,
da7fc7983d 2024-05-23 39: PermissionsExt,
da7fc7983d 2024-05-23 40: },
7620f854a7 2024-05-21 41: path::{
7620f854a7 2024-05-21 42: Path,
7620f854a7 2024-05-21 43: PathBuf
7620f854a7 2024-05-21 44: },
7620f854a7 2024-05-21 45: time::Duration,
7620f854a7 2024-05-21 46: vec::Vec,
7620f854a7 2024-05-21 47: };
7620f854a7 2024-05-21 48:
61238a3618 2024-05-22 49: fn address_into_iter<'a>(addr: &'a mail_parser::Address<'a, >) -> impl Iterator<Item = Cow<'a, str>> {
61238a3618 2024-05-22 50: addr.clone().into_list().into_iter().map(|a| a.address.unwrap())
61238a3618 2024-05-22 51: }
7620f854a7 2024-05-21 52:
e81897ec87 2024-05-24 53: async fn relay_mails(maildir: &Path, core: &TelegramTransport) -> Result<()> {
7620f854a7 2024-05-21 54: let new_dir = maildir.join("new");
7620f854a7 2024-05-21 55:
7620f854a7 2024-05-21 56: std::fs::create_dir_all(&new_dir)?;
7620f854a7 2024-05-21 57:
7620f854a7 2024-05-21 58: let files = std::fs::read_dir(new_dir)?;
7620f854a7 2024-05-21 59: for file in files {
61238a3618 2024-05-22 60: let file = file?;
f4cad2a5c0 2024-05-26 61: let mut buf: String = Default::default();
f4cad2a5c0 2024-05-26 62: std::fs::File::open(file.path())?.read_to_string(&mut buf)?;
e81897ec87 2024-05-24 63:
f4cad2a5c0 2024-05-26 64: let mail = mail_parser::MessageParser::new().parse(&buf)
f4cad2a5c0 2024-05-26 65: .ok_or(anyhow!("Failed to parse mail `{:?}`.", file))?;
e81897ec87 2024-05-24 66:
e81897ec87 2024-05-24 67: // Fetching address lists from fields we know
e81897ec87 2024-05-24 68: let mut to = HashSet::new();
e81897ec87 2024-05-24 69: if let Some(addr) = mail.to() {
e81897ec87 2024-05-24 70: let _ = address_into_iter(addr).map(|x| to.insert(x));
e81897ec87 2024-05-24 71: };
e81897ec87 2024-05-24 72: if let Some(addr) = mail.header("X-Samotop-To") {
e81897ec87 2024-05-24 73: match addr {
e81897ec87 2024-05-24 74: mail_parser::HeaderValue::Address(addr) => {
e81897ec87 2024-05-24 75: let _ = address_into_iter(addr).map(|x| to.insert(x));
e81897ec87 2024-05-24 76: },
e81897ec87 2024-05-24 77: mail_parser::HeaderValue::Text(text) => {
e81897ec87 2024-05-24 78: to.insert(text.clone());
e81897ec87 2024-05-24 79: },
e81897ec87 2024-05-24 80: _ => {}
e81897ec87 2024-05-24 81: }
e81897ec87 2024-05-24 82: };
e81897ec87 2024-05-24 83:
e81897ec87 2024-05-24 84: // Adding all known addresses to recipient list, for anyone else adding default
e81897ec87 2024-05-24 85: // Also if list is empty also adding default
f4cad2a5c0 2024-05-26 86: let mut rcpt: HashSet<&ChatId> = HashSet::new();
e81897ec87 2024-05-24 87: for item in to {
e81897ec87 2024-05-24 88: let item = item.into_owned();
e81897ec87 2024-05-24 89: match core.recipients.get(&item) {
e81897ec87 2024-05-24 90: Some(addr) => rcpt.insert(addr),
e81897ec87 2024-05-24 91: None => {
e81897ec87 2024-05-24 92: core.debug(format!("Recipient [{}] not found.", &item)).await?;
e81897ec87 2024-05-24 93: rcpt.insert(core.recipients.get("_")
e81897ec87 2024-05-24 94: .ok_or(anyhow!("Missing default address in recipient table."))?)
e81897ec87 2024-05-24 95: }
e81897ec87 2024-05-24 96: };
e81897ec87 2024-05-24 97: };
e81897ec87 2024-05-24 98: if rcpt.is_empty() {
e81897ec87 2024-05-24 99: core.debug("No recipient or envelope address.").await?;
e81897ec87 2024-05-24 100: rcpt.insert(core.recipients.get("_")
e81897ec87 2024-05-24 101: .ok_or(anyhow!("Missing default address in recipient table."))?);
e81897ec87 2024-05-24 102: };
e81897ec87 2024-05-24 103:
e81897ec87 2024-05-24 104: // prepating message header
f4cad2a5c0 2024-05-26 105: let mut reply: Vec<Cow<'_, str>> = vec![];
e81897ec87 2024-05-24 106: if let Some(subject) = mail.subject() {
e81897ec87 2024-05-24 107: reply.push(format!("**Subject:** `{}`", subject).into());
e81897ec87 2024-05-24 108: } else if let Some(thread) = mail.thread_name() {
e81897ec87 2024-05-24 109: reply.push(format!("**Thread:** `{}`", thread).into());
e81897ec87 2024-05-24 110: }
e81897ec87 2024-05-24 111: if let Some(from) = mail.from() {
e81897ec87 2024-05-24 112: reply.push(format!("**From:** `{:?}`", address_into_iter(from).collect::<Vec<_>>().join(", ")).into());
e81897ec87 2024-05-24 113: }
e81897ec87 2024-05-24 114: if let Some(sender) = mail.sender() {
e81897ec87 2024-05-24 115: reply.push(format!("**Sender:** `{:?}`", address_into_iter(sender).collect::<Vec<_>>().join(", ")).into());
e81897ec87 2024-05-24 116: }
e81897ec87 2024-05-24 117: reply.push("".into());
e81897ec87 2024-05-24 118: let header_size = reply.join("\n").len() + 1;
e81897ec87 2024-05-24 119:
e81897ec87 2024-05-24 120: let html_parts = mail.html_body_count();
e81897ec87 2024-05-24 121: let text_parts = mail.text_body_count();
e81897ec87 2024-05-24 122: let attachments = mail.attachment_count();
e81897ec87 2024-05-24 123: if html_parts != text_parts {
e81897ec87 2024-05-24 124: core.debug(format!("Hm, we have {} HTML parts and {} text parts.", html_parts, text_parts)).await?;
e81897ec87 2024-05-24 125: }
e81897ec87 2024-05-24 126: //let mut html_num = 0;
e81897ec87 2024-05-24 127: let mut text_num = 0;
e81897ec87 2024-05-24 128: let mut file_num = 0;
e81897ec87 2024-05-24 129: // let's display first html or text part as body
e81897ec87 2024-05-24 130: let mut body = "".into();
e81897ec87 2024-05-24 131: /*
e81897ec87 2024-05-24 132: * actually I don't wanna parse that html stuff
e81897ec87 2024-05-24 133: if html_parts > 0 {
e81897ec87 2024-05-24 134: let text = mail.body_html(0).unwrap();
e81897ec87 2024-05-24 135: if text.len() < 4096 - header_size {
e81897ec87 2024-05-24 136: body = text;
e81897ec87 2024-05-24 137: html_num = 1;
e81897ec87 2024-05-24 138: }
e81897ec87 2024-05-24 139: };
e81897ec87 2024-05-24 140: */
e81897ec87 2024-05-24 141: if body == "" && text_parts > 0 {
e81897ec87 2024-05-24 142: let text = mail.body_text(0)
e81897ec87 2024-05-24 143: .ok_or(anyhow!("Failed to extract text from message."))?;
e81897ec87 2024-05-24 144: if text.len() < 4096 - header_size {
e81897ec87 2024-05-24 145: body = text;
e81897ec87 2024-05-24 146: text_num = 1;
e81897ec87 2024-05-24 147: }
e81897ec87 2024-05-24 148: };
e81897ec87 2024-05-24 149: reply.push("```".into());
e81897ec87 2024-05-24 150: for line in body.lines() {
e81897ec87 2024-05-24 151: reply.push(line.into());
e81897ec87 2024-05-24 152: }
e81897ec87 2024-05-24 153: reply.push("```".into());
e81897ec87 2024-05-24 154:
e81897ec87 2024-05-24 155: // and let's collect all other attachment parts
e81897ec87 2024-05-24 156: let mut files_to_send = vec![];
e81897ec87 2024-05-24 157: /*
e81897ec87 2024-05-24 158: * let's just skip html parts for now, they just duplicate text?
e81897ec87 2024-05-24 159: while html_num < html_parts {
e81897ec87 2024-05-24 160: files_to_send.push(mail.html_part(html_num).unwrap());
e81897ec87 2024-05-24 161: html_num += 1;
e81897ec87 2024-05-24 162: }
e81897ec87 2024-05-24 163: */
e81897ec87 2024-05-24 164: while text_num < text_parts {
e81897ec87 2024-05-24 165: files_to_send.push(mail.text_part(text_num)
e81897ec87 2024-05-24 166: .ok_or(anyhow!("Failed to get text part from message"))?);
e81897ec87 2024-05-24 167: text_num += 1;
e81897ec87 2024-05-24 168: }
e81897ec87 2024-05-24 169: while file_num < attachments {
e81897ec87 2024-05-24 170: files_to_send.push(mail.attachment(file_num)
e81897ec87 2024-05-24 171: .ok_or(anyhow!("Failed to get file part from message"))?);
e81897ec87 2024-05-24 172: file_num += 1;
e81897ec87 2024-05-24 173: }
e81897ec87 2024-05-24 174:
f4cad2a5c0 2024-05-26 175: let msg = reply.join("\n");
e81897ec87 2024-05-24 176: for chat in rcpt {
f4cad2a5c0 2024-05-26 177: if !files_to_send.is_empty() {
f4cad2a5c0 2024-05-26 178: let mut files = vec![];
f4cad2a5c0 2024-05-26 179: let mut first_one = true;
f4cad2a5c0 2024-05-26 180: for chunk in &files_to_send {
f4cad2a5c0 2024-05-26 181: let data = chunk.contents();
f4cad2a5c0 2024-05-26 182: let mut filename: Option<String> = None;
f4cad2a5c0 2024-05-26 183: for header in chunk.headers() {
f4cad2a5c0 2024-05-26 184: if header.name() == "Content-Type" {
f4cad2a5c0 2024-05-26 185: match header.value() {
f4cad2a5c0 2024-05-26 186: mail_parser::HeaderValue::ContentType(contenttype) => {
f4cad2a5c0 2024-05-26 187: if let Some(fname) = contenttype.attribute("name") {
f4cad2a5c0 2024-05-26 188: filename = Some(fname.to_owned());
f4cad2a5c0 2024-05-26 189: }
f4cad2a5c0 2024-05-26 190: },
f4cad2a5c0 2024-05-26 191: _ => {
f4cad2a5c0 2024-05-26 192: core.debug("Attachment has bad ContentType header.").await?;
f4cad2a5c0 2024-05-26 193: },
f4cad2a5c0 2024-05-26 194: };
f4cad2a5c0 2024-05-26 195: };
f4cad2a5c0 2024-05-26 196: };
f4cad2a5c0 2024-05-26 197: let filename = if let Some(fname) = filename {
f4cad2a5c0 2024-05-26 198: fname
f4cad2a5c0 2024-05-26 199: } else {
f4cad2a5c0 2024-05-26 200: "Attachment.txt".into()
f4cad2a5c0 2024-05-26 201: };
f4cad2a5c0 2024-05-26 202: let item = teloxide::types::InputMediaDocument::new(
f4cad2a5c0 2024-05-26 203: teloxide::types::InputFile::memory(data.to_vec())
f4cad2a5c0 2024-05-26 204: .file_name(filename));
f4cad2a5c0 2024-05-26 205: let item = if first_one {
f4cad2a5c0 2024-05-26 206: first_one = false;
f4cad2a5c0 2024-05-26 207: item.caption(&msg).parse_mode(MarkdownV2)
f4cad2a5c0 2024-05-26 208: } else {
f4cad2a5c0 2024-05-26 209: item
f4cad2a5c0 2024-05-26 210: };
f4cad2a5c0 2024-05-26 211: files.push(teloxide::types::InputMedia::Document(item));
f4cad2a5c0 2024-05-26 212: }
f4cad2a5c0 2024-05-26 213: core.sendgroup(chat, files).await?;
f4cad2a5c0 2024-05-26 214: } else {
f4cad2a5c0 2024-05-26 215: core.send(chat, &msg).await?;
e81897ec87 2024-05-24 216: }
e81897ec87 2024-05-24 217: }
7620f854a7 2024-05-21 218:
7620f854a7 2024-05-21 219: std::fs::remove_file(file.path())?;
7620f854a7 2024-05-21 220: }
7620f854a7 2024-05-21 221: Ok(())
7620f854a7 2024-05-21 222: }
7620f854a7 2024-05-21 223:
7620f854a7 2024-05-21 224: fn my_prudence() -> Prudence {
7620f854a7 2024-05-21 225: Prudence::default().with_read_timeout(Duration::from_secs(60)).with_banner_delay(Duration::from_secs(1))
7620f854a7 2024-05-21 226: }
7620f854a7 2024-05-21 227:
61238a3618 2024-05-22 228: pub struct TelegramTransport {
f4cad2a5c0 2024-05-26 229: tg: teloxide::adaptors::DefaultParseMode<Bot>,
f4cad2a5c0 2024-05-26 230: recipients: HashMap<String, ChatId>,
61238a3618 2024-05-22 231: }
61238a3618 2024-05-22 232:
61238a3618 2024-05-22 233: impl TelegramTransport {
da7fc7983d 2024-05-23 234: pub fn new(settings: config::Config) -> TelegramTransport {
f4cad2a5c0 2024-05-26 235: let tg = Bot::new(settings.get_string("api_key")
f4cad2a5c0 2024-05-26 236: .expect("[smtp2tg.toml] missing \"api_key\" parameter.\n"))
f4cad2a5c0 2024-05-26 237: .parse_mode(MarkdownV2);
f4cad2a5c0 2024-05-26 238: let recipients: HashMap<String, ChatId> = settings.get_table("recipients")
da7fc7983d 2024-05-23 239: .expect("[smtp2tg.toml] missing table \"recipients\".\n")
f4cad2a5c0 2024-05-26 240: .into_iter().map(|(a, b)| (a, ChatId (b.into_int()
da7fc7983d 2024-05-23 241: .expect("[smtp2tg.toml] \"recipient\" table values should be integers.\n")
da7fc7983d 2024-05-23 242: ))).collect();
da7fc7983d 2024-05-23 243: if !recipients.contains_key("_") {
da7fc7983d 2024-05-23 244: eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n");
da7fc7983d 2024-05-23 245: panic!("no default recipient");
da7fc7983d 2024-05-23 246: }
61238a3618 2024-05-22 247:
61238a3618 2024-05-22 248: TelegramTransport {
7620f854a7 2024-05-21 249: tg,
7620f854a7 2024-05-21 250: recipients,
61238a3618 2024-05-22 251: }
61238a3618 2024-05-22 252: }
61238a3618 2024-05-22 253:
f4cad2a5c0 2024-05-26 254: pub async fn debug<'b, S>(&self, msg: S) -> Result<teloxide::types::Message>
f4cad2a5c0 2024-05-26 255: where S: Into<String> {
f4cad2a5c0 2024-05-26 256: task::sleep(Duration::from_secs(5)).await;
f4cad2a5c0 2024-05-26 257: Ok(self.tg.send_message(*self.recipients.get("_").unwrap(), msg).await?)
f4cad2a5c0 2024-05-26 258: }
f4cad2a5c0 2024-05-26 259:
f4cad2a5c0 2024-05-26 260: pub async fn send<'b, S>(&self, to: &ChatId, msg: S) -> Result<teloxide::types::Message>
f4cad2a5c0 2024-05-26 261: where S: Into<String> {
f4cad2a5c0 2024-05-26 262: task::sleep(Duration::from_secs(5)).await;
f4cad2a5c0 2024-05-26 263: Ok(self.tg.send_message(*to, msg).await?)
f4cad2a5c0 2024-05-26 264: }
f4cad2a5c0 2024-05-26 265:
f4cad2a5c0 2024-05-26 266: pub async fn sendgroup<M>(&self, to: &ChatId, media: M) -> Result<Vec<teloxide::types::Message>>
f4cad2a5c0 2024-05-26 267: where M: IntoIterator<Item = teloxide::types::InputMedia> {
f4cad2a5c0 2024-05-26 268: task::sleep(Duration::from_secs(5)).await;
f4cad2a5c0 2024-05-26 269: Ok(self.tg.send_media_group(*to, media).await?)
7620f854a7 2024-05-21 270: }
7620f854a7 2024-05-21 271: }
7620f854a7 2024-05-21 272:
7620f854a7 2024-05-21 273: #[async_std::main]
7620f854a7 2024-05-21 274: async fn main() {
7620f854a7 2024-05-21 275: let settings: config::Config = config::Config::builder()
7620f854a7 2024-05-21 276: .add_source(config::File::with_name("smtp2tg.toml"))
da7fc7983d 2024-05-23 277: .build()
da7fc7983d 2024-05-23 278: .expect("[smtp2tg.toml] there was an error reading config\n\
da7fc7983d 2024-05-23 279: \tplease consult \"smtp2tg.toml.example\" for details");
7620f854a7 2024-05-21 280:
da7fc7983d 2024-05-23 281: let maildir: PathBuf = settings.get_string("maildir")
da7fc7983d 2024-05-23 282: .expect("[smtp2tg.toml] missing \"maildir\" parameter.\n").into();
da7fc7983d 2024-05-23 283: let listen_on = settings.get_string("listen_on")
da7fc7983d 2024-05-23 284: .expect("[smtp2tg.toml] missing \"listen_on\" parameter.\n");
da7fc7983d 2024-05-23 285: let core = TelegramTransport::new(settings);
7620f854a7 2024-05-21 286: let sink = Builder + Name::new("smtp2tg") + DebugService +
51adce1e7e 2024-05-22 287: my_prudence() + MailDir::new(maildir.clone()).unwrap();
51adce1e7e 2024-05-22 288:
31aec3c4b0 2024-05-23 289: env_logger::init();
31aec3c4b0 2024-05-23 290:
7620f854a7 2024-05-21 291: task::spawn(async move {
7620f854a7 2024-05-21 292: loop {
e81897ec87 2024-05-24 293: // relay mails
e81897ec87 2024-05-24 294: if let Err(err) = relay_mails(&maildir, &core).await {
e81897ec87 2024-05-24 295: // in case that fails - inform default recipient
e81897ec87 2024-05-24 296: if let Err(err) = core.debug(format!("Sending emails failed:\n{:?}", err)).await {
e81897ec87 2024-05-24 297: // in case that also fails - write some logs and bail
e81897ec87 2024-05-24 298: eprintln!("Failed to contact Telegram:\n{:?}", err);
e81897ec87 2024-05-24 299: task::sleep(Duration::from_secs(5 * 60)).await;
e81897ec87 2024-05-24 300: };
e81897ec87 2024-05-24 301: };
7620f854a7 2024-05-21 302: task::sleep(Duration::from_secs(5)).await;
7620f854a7 2024-05-21 303: }
7620f854a7 2024-05-21 304: });
7620f854a7 2024-05-21 305:
7620f854a7 2024-05-21 306: match listen_on.as_str() {
51adce1e7e 2024-05-22 307: "socket" => {
da7fc7983d 2024-05-23 308: let socket_path = "./smtp2tg.sock";
da7fc7983d 2024-05-23 309: match std::fs::symlink_metadata(socket_path) {
da7fc7983d 2024-05-23 310: Ok(metadata) => {
da7fc7983d 2024-05-23 311: if metadata.file_type().is_socket() {
da7fc7983d 2024-05-23 312: std::fs::remove_file(socket_path)
da7fc7983d 2024-05-23 313: .expect("[smtp2tg] failed to remove old socket.\n");
da7fc7983d 2024-05-23 314: } else {
da7fc7983d 2024-05-23 315: eprintln!("[smtp2tg] \"./smtp2tg.sock\" we wanted to use is actually not a socket.\n\
da7fc7983d 2024-05-23 316: [smtp2tg] please check the file and remove it manually.\n");
da7fc7983d 2024-05-23 317: panic!("socket path unavailable");
da7fc7983d 2024-05-23 318: }
da7fc7983d 2024-05-23 319: },
da7fc7983d 2024-05-23 320: Err(err) => {
da7fc7983d 2024-05-23 321: match err.kind() {
da7fc7983d 2024-05-23 322: std::io::ErrorKind::NotFound => {},
da7fc7983d 2024-05-23 323: _ => {
da7fc7983d 2024-05-23 324: eprintln!("{:?}", err);
da7fc7983d 2024-05-23 325: panic!("unhandled file type error");
da7fc7983d 2024-05-23 326: }
da7fc7983d 2024-05-23 327: };
da7fc7983d 2024-05-23 328: }
da7fc7983d 2024-05-23 329: };
da7fc7983d 2024-05-23 330:
51adce1e7e 2024-05-22 331: let sink = sink + samotop::smtp::Lmtp.with(SmtpParser);
da7fc7983d 2024-05-23 332: task::spawn(async move {
da7fc7983d 2024-05-23 333: // Postpone mode change on the socket. I can't actually change
da7fc7983d 2024-05-23 334: // other way, as UnixServer just grabs path, and blocks
da7fc7983d 2024-05-23 335: task::sleep(Duration::from_secs(1)).await;
da7fc7983d 2024-05-23 336: std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o777)).unwrap();
da7fc7983d 2024-05-23 337: });
da7fc7983d 2024-05-23 338: samotop::server::UnixServer::on(socket_path)
51adce1e7e 2024-05-22 339: .serve(sink.build()).await.unwrap();
51adce1e7e 2024-05-22 340: },
51adce1e7e 2024-05-22 341: _ => {
51adce1e7e 2024-05-22 342: let sink = sink + samotop::smtp::Esmtp.with(SmtpParser);
51adce1e7e 2024-05-22 343: samotop::server::TcpServer::on(listen_on)
51adce1e7e 2024-05-22 344: .serve(sink.build()).await.unwrap();
51adce1e7e 2024-05-22 345: },
61238a3618 2024-05-22 346: };
7620f854a7 2024-05-21 347: }