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