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