Check-in [61238a3618]
Logged in as anonymous
Overview
Comment:well, it kindda works...
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 61238a36184dd4245be3d682efe52bfa2cf745fbbe60547ba353259461030eca
User & Date: arcade on 2024-05-22 13:00:06.874
Other Links: manifest | tags
Context
2024-05-22
13:31
use LMTP for socket server check-in: 51adce1e7e user: arcade tags: trunk
13:00
well, it kindda works... check-in: 61238a3618 user: arcade tags: trunk
2024-05-21
15:15
first attempt check-in: 7620f854a7 user: arcade tags: trunk
Changes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9

10
11
12



13
14
15
16
17









-



-
-
-





[package]
name = "smtp2tg"
version = "0.1.0"
authors = [ "arcade" ]
edition = "2021"

[dependencies]
anyhow = "*"
async-std = { version = "*", features = [ "tokio1" ] }
async-trait = "*"
config = { version = "*", default-features = false, features = [ "toml" ] }
telegram-bot = { git = "https://github.com/telegram-rs/telegram-bot" }
mail-parser = { version = "*", features = ["serde", "serde_support"] }
#mail-parser = "*"
#rust-smtp-server = "*"
#tokio = "*"
samotop = "*"

[profile.release]
lto = true
codegen-units = 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24




25
26
27
28
29
30
31
32
33


34

35
36
37

38
39
40
41
42
43
44
45
46
47
48
49
50

51

52

53
54
55
56
57
58















59
60








































































61


















62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

78
79
80
81
82
83
84


85
86
87
88



89
90
91

92
93
94
95

96
97
98
99
100
101








102
103
104
105
106
107



108
109
110
111
112
113
114
115
116
117
118
119

120
121
122
123
124

125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142
143

144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
1
2



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

36


37
38
39
40
41
42
43
44

45
46
47
48
49

50
51
52

53






54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175

176
177
178
179
180
181


182
183
184
185


186
187
188
189
190

191
192
193
194

195
196
197
198
199
200

201
202
203
204
205
206
207
208
209
210
211



212
213
214

215
216
217
218
219
220
221
222
223
224

225
226

227
228
229
230
231
232
233
234

235
236
237
238
239
240
241
242
243
244
245




246






















-
-
-


















-
+
+
+
+









+
+
-
+
-
-

+






-





-
+

+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+















-
+





-
-
+
+


-
-
+
+
+


-
+



-
+





-
+
+
+
+
+
+
+
+



-
-
-
+
+
+
-










-
+

-



+




-

+









-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
use anyhow::Result;
use async_std::task;
//use async_trait::async_trait;
//use futures::io::AsyncRead;
//use mail_parser::Message;
use samotop::{
	mail::{
		Builder,
		DebugService,
		MailDir,
		Name
	},
	smtp::Prudence,
};
use telegram_bot::{
	Api,
	ParseMode,
	SendMessage,
	UserId,
};

use std::{
	borrow::Cow,
	collections::HashMap,
	collections::{
		HashMap,
		HashSet,
	},
	io::Read,
	path::{
		Path,
		PathBuf
	},
	time::Duration,
	vec::Vec,
};

fn address_into_iter<'a>(addr: &'a mail_parser::Address<'a, >) -> impl Iterator<Item = Cow<'a, str>> {
	addr.clone().into_list().into_iter().map(|a| a.address.unwrap())

}
fn relay_mails(maildir: &Path, core: &Core) -> Result<()> {
	use mail_parser::*;

fn relay_mails(maildir: &Path, core: &TelegramTransport) -> Result<()> {
	let new_dir = maildir.join("new");

	std::fs::create_dir_all(&new_dir)?;

	let files = std::fs::read_dir(new_dir)?;
	for file in files {
		dbg!(&file);
		let file = file?;
		let mut buf = Vec::new();
		std::fs::File::open(file.path())?.read_to_end(&mut buf)?;

		task::block_on(async move {
			match MessageParser::default().parse(&buf[..]) {
			match mail_parser::MessageParser::default().parse(&buf[..]) {
				Some(mail) => {
					let mail = mail.clone();
					/*

					dbg!(&mail);
					let to = match mail.to() {
						Some(mail) => mail.into_list().into_iter().map(|a| a.address.unwrap()).collect(),
						None => match mail.header("X-Samotop-To").unwrap() {
							mail_parser::HeaderValue::Address(addr) => addr.address.unwrap(),
						},
					// Fetching address lists from fields we know
					let mut to = HashSet::new();
					if let Some(addr) = mail.to() {
						let _ = address_into_iter(addr).map(|x| to.insert(x));
					};
					if let Some(addr) = mail.header("X-Samotop-To") {
						match addr {
							mail_parser::HeaderValue::Address(addr) => {
								let _ = address_into_iter(addr).map(|x| to.insert(x));
							},
							mail_parser::HeaderValue::Text(text) => {
								to.insert(text.clone());
							},
							_ => {}
						}
					};
					dbg!(&to);

					// Adding all known addresses to recipient list, for anyone else adding default
					// Also if list is empty also adding default
					let mut rcpt: HashSet<UserId> = HashSet::new();
					for item in to {
						let item = item.into_owned();
						if core.recipients.contains_key(&item) {
							rcpt.insert(core.recipients[&item]);
						} else {
							core.debug(format!("Recipient [{}] not found.", &item)).await.unwrap();
							rcpt.insert(core.default);
						}
					};
					if rcpt.is_empty() {
						rcpt.insert(core.default);
						core.debug("No recipient or envelope address.").await.unwrap();
					};

					// prepating message header
					let mut reply: Vec<Cow<str>> = vec![];
					if let Some(subject) = mail.subject() {
						reply.push(format!("<b>Subject:</b> {}", subject).into());
					} else if let Some(thread) = mail.thread_name() {
						reply.push(format!("<b>Thread:</b> {}", thread).into());
					}
					if let Some(from) = mail.from() {
						reply.push(format!("<b>From:</b> {:?}", from).into());
					}
					if let Some(sender) = mail.sender() {
						reply.push(format!("<b>Sender:</b> {:?}", sender).into());
					}
					reply.push("".into());
					let header_size = reply.join("\n").len() + 1;

					let html_parts = mail.html_body_count();
					let text_parts = mail.text_body_count();
					let attachments = mail.attachment_count();
					if html_parts != text_parts {
						core.debug(format!("Hm, we have {} HTML parts and {} text parts.", html_parts, text_parts)).await.unwrap();
					}
					//let mut html_num = 0;
					let mut text_num = 0;
					let mut file_num = 0;
					// let's display first html or text part as body
					let mut body = "".into();
					/*
					 * actually I don't wanna parse that html stuff
					if html_parts > 0 {
						let text = mail.body_html(0).unwrap();
						if text.len() < 4096 - header_size {
							body = text;
							html_num = 1;
						}
					};
					*/
					if body == "" && text_parts > 0 {
						let text = mail.body_text(0).unwrap();
						if text.len() < 4096 - header_size {
							body = text;
							text_num = 1;
						}
					};
					reply.push(body);

					// and let's coillect all other attachment parts
					let mut files_to_send = vec![];
					/*
					 * let's just skip html parts for now, they just duplicate text?
					while html_num < html_parts {
						files_to_send.push(mail.html_part(html_num).unwrap());
						html_num += 1;
					}
					*/
					while text_num < text_parts {
						files_to_send.push(mail.text_part(text_num).unwrap());
						text_num += 1;
					}
					while file_num < attachments {
						files_to_send.push(mail.attachment(file_num).unwrap());
						file_num += 1;
					}

					for chat in rcpt {
						core.send(chat, reply.join("\n")).await.unwrap();
						for chunk in &files_to_send {
							task::sleep(Duration::from_secs(5)).await;
							let data = chunk.contents().to_vec();
							let obj = telegram_bot::types::InputFileUpload::with_data(data, "Attachment");
							core.sendfile(chat, obj).await.unwrap();
						}
					}
				},
				None => { core.debug("None mail.").await.unwrap(); },
				//send_to_sendgrid(mail, sendgrid_api_key).await;
			};
		});

		std::fs::remove_file(file.path())?;
	}
	Ok(())
}

fn my_prudence() -> Prudence {
	Prudence::default().with_read_timeout(Duration::from_secs(60)).with_banner_delay(Duration::from_secs(1))
}

pub struct Core {
pub struct TelegramTransport {
	default: UserId,
	tg: Api,
	recipients: HashMap<String, UserId>,
}

impl Core {
	pub fn new(settings: &config::Config) -> Result<Core> {
impl TelegramTransport {
	pub fn new(settings: &config::Config) -> TelegramTransport {
		let api_key = settings.get_string("api_key").unwrap();
		let tg = Api::new(api_key);
		let default_recipient = settings.get_string("default")?;
		let recipients: HashMap<String, UserId> = settings.get_table("recipients")?.into_iter().map(|(a, b)| (a, UserId::new(b.into_int().unwrap()))).collect();
		let default_recipient = settings.get_string("default").unwrap();
		let recipients: HashMap<String, UserId> = settings.get_table("recipients").unwrap().into_iter().map(|(a, b)| (a, UserId::new(b.into_int().unwrap()))).collect();
		// Barf if no default
		let default = recipients[&default_recipient];

		Ok(Core {
		TelegramTransport {
			default,
			tg,
			recipients,
		})
		}
	}

	pub async fn debug<'b, S>(&self, msg: S) -> Result<()>
	where S: Into<Cow<'b, str>> {
		self.tg.send(SendMessage::new(self.default, msg)
			.parse_mode(ParseMode::Markdown)).await?;
			.parse_mode(ParseMode::Html)).await?;
		Ok(())
	}

	pub async fn send<'b, S>(&self, to: UserId, msg: S) -> Result<()>
	where S: Into<Cow<'b, str>> {
		self.tg.send(SendMessage::new(to, msg)
			.parse_mode(ParseMode::Html)).await?;
		Ok(())
	}

	pub async fn send<'b, S>(&self, to: String, msg: S) -> Result<()>
	where S: Into<Cow<'b, str>> {
		self.tg.send(SendMessage::new(self.recipients[&to], msg)
	pub async fn sendfile<V>(&self, to: UserId, chunk: V) -> Result<()>
	where V: Into<telegram_bot::InputFile> {
		self.tg.send(telegram_bot::SendDocument::new(to, chunk)).await?;
			.parse_mode(ParseMode::Markdown)).await?;
		Ok(())
	}
}

#[async_std::main]
async fn main() {
	let settings: config::Config = config::Config::builder()
		.add_source(config::File::with_name("smtp2tg.toml"))
		.build().unwrap();

	let core = Core::new(&settings).unwrap();
	let core = TelegramTransport::new(&settings);
	let maildir: PathBuf = settings.get_string("maildir").unwrap().into();
	let addr = "./smtp2tg.sock";
	let listen_on = settings.get_string("listen_on").unwrap();
	let sink = Builder + Name::new("smtp2tg") + DebugService +
		samotop::smtp::Esmtp.with(samotop::smtp::SmtpParser) + my_prudence() +
		//TelegramTransport::new(&settings);
		MailDir::new(maildir.clone()).unwrap();

	task::spawn(async move {
		loop {
			task::sleep(Duration::from_secs(5)).await;
			relay_mails(&maildir, &core).unwrap();
			task::sleep(Duration::from_secs(5)).await;
		}
	});

	match listen_on.as_str() {
		"socket" => samotop::server::UnixServer::on("./smtp2tg.sock")
			.serve(sink.build()).await.unwrap(),
		_ => samotop::server::TcpServer::on(listen_on)
			.serve(sink.build()).await.unwrap(),
	};
	/*
	task::block_on(async {
		let be = MyBackend;

}
		//let mut s = Server::new(be);

		s.addr = "127.0.0.1:2525".to_string();
		s.domain = "localhost".to_string();
		s.read_timeout = std::time::Duration::from_secs(10);
		s.write_timeout = std::time::Duration::from_secs(10);
		s.max_message_bytes = 10 * 1024 * 1024;
		s.max_recipients = 50;
		s.max_line_length = 1000;
		s.allow_insecure_auth = true;

		println!("Starting server on {}", s.addr);
		match s.listen_and_serve().await {
			Ok(_) => println!("Server stopped"),
			Err(e) => println!("Server error: {}", e),
		}
		Ok(())
	})
	*/
}