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