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