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