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