Lines of
src/main.rs
from check-in 7f8a9994b3
that are changed by the sequence of edits moving toward
check-in 562951709a:
1: //! Simple SMTP-to-Telegram gateway. Can parse email and send them as telegram
2: //! messages to specified chats, generally you specify which email address is
3: //! available in configuration, everything else is sent to default address.
4:
5: use anyhow::{
6: anyhow,
7: bail,
8: Result,
9: };
10: use async_std::{
11: io::Error,
12: task,
13: };
14: use mailin_embedded::{
15: Response,
16: response::*,
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: },
38: vec::Vec,
39: };
40:
41: /// `SomeHeaders` object to store data through SMTP session
42: #[derive(Clone, Debug)]
43: struct SomeHeaders {
44: from: String,
45: to: Vec<String>,
46: }
47:
48: /// `TelegramTransport` Central object with TG api and configuration
49: #[derive(Clone)]
50: struct TelegramTransport {
51: data: Vec<u8>,
52: headers: Option<SomeHeaders>,
53: recipients: HashMap<String, ChatId>,
54: relay: bool,
55: tg: teloxide::adaptors::DefaultParseMode<teloxide::adaptors::Throttle<Bot>>,
56: }
57:
58: impl TelegramTransport {
59: /// Initialize API and read configuration
60: fn new(settings: config::Config) -> TelegramTransport {
61: let tg = Bot::new(settings.get_string("api_key")
62: .expect("[smtp2tg.toml] missing \"api_key\" parameter.\n"))
63: .throttle(teloxide::adaptors::throttle::Limits::default())
64: .parse_mode(MarkdownV2);
65: let recipients: HashMap<String, ChatId> = settings.get_table("recipients")
66: .expect("[smtp2tg.toml] missing table \"recipients\".\n")
67: .into_iter().map(|(a, b)| (a, ChatId (b.into_int()
68: .expect("[smtp2tg.toml] \"recipient\" table values should be integers.\n")
69: ))).collect();
70: if !recipients.contains_key("_") {
71: eprintln!("[smtp2tg.toml] \"recipient\" table misses \"default_recipient\".\n");
72: panic!("no default recipient");
73: }
74: let value = settings.get_string("unknown");
75: let relay = match value {
76: Ok(value) => {
77: match value.as_str() {
78: "relay" => true,
79: "deny" => false,
80: _ => {
81: eprintln!("[smtp2tg.toml] \"unknown\" should be either \"relay\" or \"deny\".\n");
82: panic!("bad setting");
83: },
84: }
85: },
86: Err(err) => {
87: eprintln!("[smtp2tg.toml] can't get \"unknown\":\n {}\n", err);
88: panic!("bad setting");
89: },
90: };
91:
92: TelegramTransport {
93: data: vec!(),
94: headers: None,
95: recipients,
96: relay,
97: tg,
98: }
99: }
100:
101: /// Send message to default user, used for debug/log/info purposes
102: async fn debug<'b, S>(&self, msg: S) -> Result<Message>
103: where S: Into<String> {
104: Ok(self.tg.send_message(*self.recipients.get("_").unwrap(), msg).await?)
105: }
106:
107: /// Send message to specified user
108: async fn send<'b, S>(&self, to: &ChatId, msg: S) -> Result<Message>
109: where S: Into<String> {
110: Ok(self.tg.send_message(*to, msg).await?)
111: }
112:
113: /// Attempt to deliver one message
114: async fn relay_mail (&self) -> Result<()> {
115: if let Some(headers) = &self.headers {
116: let mail = mail_parser::MessageParser::new().parse(&self.data)
117: .ok_or(anyhow!("Failed to parse mail"))?;
118:
119: // Adding all known addresses to recipient list, for anyone else adding default
120: // Also if list is empty also adding default
121: let mut rcpt: HashSet<&ChatId> = HashSet::new();
122: if headers.to.is_empty() {
123: bail!("No recipient addresses.");
124: }
125: for item in &headers.to {
126: match self.recipients.get(item) {
127: Some(addr) => rcpt.insert(addr),
128: None => {
129: self.debug(format!("Recipient [{}] not found\\.", &item)).await?;
130: rcpt.insert(self.recipients.get("_")
131: .ok_or(anyhow!("Missing default address in recipient table\\."))?)
132: }
133: };
134: };
135: if rcpt.is_empty() {
136: self.debug("No recipient or envelope address\\.").await?;
137: rcpt.insert(self.recipients.get("_")
138: .ok_or(anyhow!("Missing default address in recipient table."))?);
139: };
140:
141: // prepating message header
142: let mut reply: Vec<Cow<'_, str>> = vec![];
143: if let Some(subject) = mail.subject() {
144: reply.push(format!("**Subject:** `{}`", subject).into());
145: } else if let Some(thread) = mail.thread_name() {
146: reply.push(format!("**Thread:** `{}`", thread).into());
147: }
148: reply.push(format!("**From:** `{}`", headers.from).into());
149: reply.push("".into());
150: let header_size = reply.join("\n").len() + 1;
151:
152: let html_parts = mail.html_body_count();
153: let text_parts = mail.text_body_count();
154: let attachments = mail.attachment_count();
155: if html_parts != text_parts {
156: self.debug(format!("Hm, we have {} HTML parts and {} text parts\\.", html_parts, text_parts)).await?;
157: }
158: //let mut html_num = 0;
159: let mut text_num = 0;
160: let mut file_num = 0;
161: // let's display first html or text part as body
162: let mut body = "".into();
163: /*
164: * actually I don't wanna parse that html stuff
165: if html_parts > 0 {
166: let text = mail.body_html(0).unwrap();
167: if text.len() < 4096 - header_size {
168: body = text;
169: html_num = 1;
170: }
171: };
172: */
173: if body == "" && text_parts > 0 {
174: let text = mail.body_text(0)
175: .ok_or(anyhow!("Failed to extract text from message."))?;
176: if text.len() < 4096 - header_size {
177: body = text;
178: text_num = 1;
179: }
180: };
181: reply.push("```".into());
182: reply.extend(body.lines().map(|x| x.into()));
183: reply.push("```".into());
184:
185: // and let's collect all other attachment parts
186: let mut files_to_send = vec![];
187: /*
188: * let's just skip html parts for now, they just duplicate text?
189: while html_num < html_parts {
190: files_to_send.push(mail.html_part(html_num).unwrap());
191: html_num += 1;
192: }
193: */
194: while text_num < text_parts {
195: files_to_send.push(mail.text_part(text_num)
196: .ok_or(anyhow!("Failed to get text part from message"))?);
197: text_num += 1;
198: }
199: while file_num < attachments {
200: files_to_send.push(mail.attachment(file_num)
201: .ok_or(anyhow!("Failed to get file part from message"))?);
202: file_num += 1;
203: }
204:
205: let msg = reply.join("\n");
206: for chat in rcpt {
207: if !files_to_send.is_empty() {
208: let mut files = vec![];
209: let mut first_one = true;
210: for chunk in &files_to_send {
211: let data = chunk.contents();
212: let mut filename: Option<String> = None;
213: for header in chunk.headers() {
214: if header.name() == "Content-Type" {
215: match header.value() {
216: mail_parser::HeaderValue::ContentType(contenttype) => {
217: if let Some(fname) = contenttype.attribute("name") {
218: filename = Some(fname.to_owned());
219: }
220: },
221: _ => {
7f8a9994b3 2024-12-10 222: self.debug("Attachment has bad ContentType header.").await?;
223: },
224: };
225: };
226: };
227: let filename = if let Some(fname) = filename {
228: fname
229: } else {
230: "Attachment.txt".into()
231: };
232: let item = teloxide::types::InputMediaDocument::new(
233: teloxide::types::InputFile::memory(data.to_vec())
234: .file_name(filename));
235: let item = if first_one {
236: first_one = false;
237: item.caption(&msg).parse_mode(MarkdownV2)
238: } else {
239: item
240: };
241: files.push(InputMedia::Document(item));
242: }
243: self.sendgroup(chat, files).await?;
244: } else {
245: self.send(chat, &msg).await?;
246: }
247: }
248: } else {
249: bail!("No headers.");
250: }
251: Ok(())
252: }
253:
254: /// Send media to specified user
255: pub async fn sendgroup<M>(&self, to: &ChatId, media: M) -> Result<Vec<Message>>
256: where M: IntoIterator<Item = InputMedia> {
257: Ok(self.tg.send_media_group(*to, media).await?)
258: }
259: }
260:
261: impl mailin_embedded::Handler for TelegramTransport {
262: /// Just deny login auth
263: fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
264: INVALID_CREDENTIALS
265: }
266:
267: /// Just deny plain auth
268: fn auth_plain (&mut self, _authorization_id: &str, _authentication_id: &str, _password: &str) -> Response {
269: INVALID_CREDENTIALS
270: }
271:
272: /// Verify whether address is deliverable
273: fn rcpt (&mut self, to: &str) -> Response {
274: if self.relay {
275: OK
276: } else {
277: match self.recipients.get(to) {
278: Some(_) => OK,
279: None => {
280: if self.relay {
281: OK
282: } else {
283: NO_MAILBOX
284: }
285: }
286: }
287: }
288: }
289:
290: /// Save headers we need
291: fn data_start (&mut self, _domain: &str, from: &str, _is8bit: bool, to: &[String]) -> Response {
292: self.headers = Some(SomeHeaders{
293: from: from.to_string(),
294: to: to.to_vec(),
295: });
296: OK
297: }
298:
299: /// Save chunk(?) of data
300: fn data(&mut self, buf: &[u8]) -> Result<(), Error> {
301: self.data.append(buf.to_vec().as_mut());
302: Ok(())
303: }
304:
305: /// Attempt to send email, return temporary error if that fails
306: fn data_end(&mut self) -> Response {
307: let mut result = OK;
308: task::block_on(async {
309: // relay mail
310: if let Err(err) = self.relay_mail().await {
311: result = INTERNAL_ERROR;
312: // in case that fails - inform default recipient
313: if let Err(err) = self.debug(format!("Sending emails failed:\n{:?}", err)).await {
314: // in case that also fails - write some logs and bail
315: eprintln!("Failed to contact Telegram:\n{:?}", err);
316: };
317: };
318: });
319: // clear - just in case
320: self.data = vec![];
321: self.headers = None;
322: result
323: }
324: }
325:
326: #[async_std::main]
327: async fn main() -> Result<()> {
328: let settings: config::Config = config::Config::builder()
329: .set_default("listen_on", "0.0.0.0:1025").unwrap()
330: .set_default("hostname", "smtp.2.tg").unwrap()
331: .set_default("unknown", "relay").unwrap()
332: .add_source(config::File::with_name("smtp2tg.toml"))
333: .build()
334: .expect("[smtp2tg.toml] there was an error reading config\n\
335: \tplease consult \"smtp2tg.toml.example\" for details");
336:
337: let listen_on = settings.get_string("listen_on")?;
338: let server_name = settings.get_string("hostname")?;
339: let core = TelegramTransport::new(settings);
340: let mut server = mailin_embedded::Server::new(core);
341:
342: server.with_name(server_name)
343: .with_ssl(mailin_embedded::SslConfig::None).unwrap()
344: .with_addr(listen_on).unwrap();
345: server.serve().unwrap();
346:
347: Ok(())
348: }