From 53dcb6fd6527a0aec3cc25b285e555063784d7c2 Mon Sep 17 00:00:00 2001 From: Khushal Chitturi Date: Wed, 4 Mar 2026 02:14:47 +0530 Subject: [PATCH 1/2] feat: Mirror status updates onto discord --- .env.sample | 2 + Cargo.lock | 286 +++++++++++++++++++++++++++++- Cargo.toml | 3 + src/config.rs | 3 +- src/graphql/models.rs | 3 + src/graphql/queries.rs | 1 + src/ids.rs | 10 ++ src/tasks/mod.rs | 8 +- src/tasks/status_update.rs | 2 +- src/tasks/status_update_mirror.rs | 227 ++++++++++++++++++++++++ 10 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 src/tasks/status_update_mirror.rs diff --git a/.env.sample b/.env.sample index 77eced7..8961c88 100644 --- a/.env.sample +++ b/.env.sample @@ -4,3 +4,5 @@ OWNER_ID= DEBUG=true ENABLE_DEBUG_LIBRARIES=false AMD_API_KEY= +APP_PASSWORD= +EMAIL_ID= \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 36e3a8c..b8cb852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "amd" version = "1.4.0" @@ -35,6 +41,9 @@ dependencies = [ "chrono", "chrono-tz", "dotenv", + "html2text", + "imap", + "mailparse", "poise", "reqwest 0.12.23", "serde", @@ -140,6 +149,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "bumpalo" version = "3.16.0" @@ -210,6 +225,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.42" @@ -231,7 +256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", - "phf", + "phf 0.12.1", ] [[package]] @@ -487,6 +512,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -656,6 +691,39 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "html2text" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "thiserror", + "unicode-width", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "http" version = "0.2.12" @@ -1009,6 +1077,32 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imap" +version = "3.0.0-alpha.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b81eb9a89c9a40e9d6c670d9b3c4cda734573592bd49b7cd906152c95d9af2" +dependencies = [ + "base64 0.22.1", + "bufstream", + "chrono", + "imap-proto", + "lazy_static", + "native-tls", + "nom", + "ouroboros", + "regex", +] + +[[package]] +name = "imap-proto" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1f9b30846c3d04371159ef3a0413ce7c1ae0a8c619cd255c60b3d902553f22" +dependencies = [ + "nom", +] + [[package]] name = "indexmap" version = "2.7.1" @@ -1108,6 +1202,37 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mailparse" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da03d5980411a724e8aaf7b61a7b5e386ec55a7fb49ee3d0ff79efc7e5e7c7e" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1154,6 +1279,12 @@ dependencies = [ "triomphe", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -1191,6 +1322,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -1274,6 +1421,30 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.96", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1303,13 +1474,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ - "phf_shared", + "phf_shared 0.12.1", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", ] [[package]] @@ -1383,6 +1592,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1392,6 +1607,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "version_check", + "yansi", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -1412,6 +1640,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "rand" version = "0.8.5" @@ -1962,6 +2196,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2084,6 +2343,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2447,6 +2717,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "untrusted" version = "0.9.0" @@ -2870,6 +3146,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 147c7ef..12a811c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ dotenv = "0.15.0" serenity = { version = "0.12.4", features = ["chrono"] } poise = "0.6.1" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } +imap = "3.0.0-alpha.12" +mailparse = "0.15" +html2text = "0.12" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index fa87981..ad63e4f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,8 +47,7 @@ impl Default for Config { owner_id: parse_owner_id_env("OWNER_ID"), prefix_string: String::from("$"), root_url: std::env::var("ROOT_URL").expect("ROOT_URL was not found in env"), - api_key: std::env::var("AMD_API_KEY") - .expect("AMD_API_KEY was not found in env"), + api_key: std::env::var("AMD_API_KEY").expect("AMD_API_KEY was not found in env"), } } } diff --git a/src/graphql/models.rs b/src/graphql/models.rs index 89002a8..4a0892f 100644 --- a/src/graphql/models.rs +++ b/src/graphql/models.rs @@ -52,6 +52,9 @@ pub struct Member { pub track: Option, pub year: Option, pub status: Option, + #[serde(rename = "groupId")] + pub group_id: Option, + pub email: String, } #[derive(Debug, Deserialize, Clone)] diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index e4ef140..7d118c6 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -46,6 +46,7 @@ impl GraphQLClient { } track year + email } }"#; diff --git a/src/ids.rs b/src/ids.rs index a8ba4f3..df3780d 100644 --- a/src/ids.rs +++ b/src/ids.rs @@ -37,3 +37,13 @@ pub const WEB_ROLE_ID: u64 = 1298553910167994428; pub const STATUS_UPDATE_CHANNEL_ID: u64 = 764575524127244318; pub const THE_LAB_CHANNEL_ID: u64 = 1208438766893670451; + +pub const AI_STATUS_UPDATE_CHANNEL_ID: u64 = 1343489220068507649; +pub const MOBILE_STATUS_UPDATE_CHANNEL_ID: u64 = 1378685538835365960; +pub const WEB_STATUS_UPDATE_CHANNEL_ID: u64 = 1378685360133115944; +pub const SYSTEMS_STATUS_UPDATE_CHANNEL_ID: u64 = 1378426650152271902; + +pub const GROUP_ONE_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098248293716008; +pub const GROUP_TWO_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098298935738489; +pub const GROUP_THREE_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098353378070710; +pub const GROUP_FOUR_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098407216156712; diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index f4f1b8b..3f22942 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -18,6 +18,7 @@ along with this program. If not, see . //! A trait to define a job that needs to be executed regularly, for example checking for status updates daily. mod lab_attendance; mod status_update; +mod status_update_mirror; use std::fmt::{self, Debug}; @@ -26,6 +27,7 @@ use async_trait::async_trait; use lab_attendance::PresenseReport; use serenity::client::Context; use status_update::StatusUpdateReport; +use status_update_mirror::MirrorNewUpdates; use tokio::time::Duration; use crate::graphql::GraphQLClient; @@ -52,5 +54,9 @@ impl Debug for Box { /// Analogous to [`crate::commands::get_commands`], every task that is defined /// must be included in the returned vector in order for it to be scheduled. pub fn get_tasks() -> Vec> { - vec![Box::new(PresenseReport), Box::new(StatusUpdateReport)] + vec![ + Box::new(PresenseReport), + Box::new(StatusUpdateReport), + Box::new(MirrorNewUpdates), + ] } diff --git a/src/tasks/status_update.rs b/src/tasks/status_update.rs index c17bf35..3999610 100644 --- a/src/tasks/status_update.rs +++ b/src/tasks/status_update.rs @@ -15,7 +15,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use serenity::all::{CacheHttp, ChannelId, Context, CreateEmbed, CreateMessage, GuildId}; use serenity::async_trait; diff --git a/src/tasks/status_update_mirror.rs b/src/tasks/status_update_mirror.rs new file mode 100644 index 0000000..f3cca43 --- /dev/null +++ b/src/tasks/status_update_mirror.rs @@ -0,0 +1,227 @@ +/* +amFOSS Daemon: A discord bot for the amFOSS Discord server. +Copyright (C) 2024 amFOSS + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +use super::Task; +use crate::graphql::GraphQLClient; +use anyhow::Context; +use async_trait::async_trait; +use serenity::client::Context as ClientContext; +use serenity::prelude::CacheHttp; +use std::collections::HashMap; + +pub struct MirrorNewUpdates; + +use crate::utils::time::time_until; +use chrono::{Datelike, Duration, Local, Timelike}; +use chrono_tz::Asia::Kolkata; +use mailparse::{MailHeaderMap, ParsedMail}; +use poise::serenity_prelude::ChannelId; +use poise::serenity_prelude::CreateEmbed; +use poise::serenity_prelude::CreateMessage; + +pub struct EmailDetails { + pub from: String, + pub body: String, +} + +use crate::ids::{ + AI_STATUS_UPDATE_CHANNEL_ID, GROUP_FOUR_STATUS_UPDATE_CHANNEL_ID, + GROUP_ONE_STATUS_UPDATE_CHANNEL_ID, GROUP_THREE_STATUS_UPDATE_CHANNEL_ID, + GROUP_TWO_STATUS_UPDATE_CHANNEL_ID, MOBILE_STATUS_UPDATE_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, + SYSTEMS_STATUS_UPDATE_CHANNEL_ID, WEB_STATUS_UPDATE_CHANNEL_ID, +}; + +#[async_trait] +impl Task for MirrorNewUpdates { + fn name(&self) -> &str { + "mirror_new_updates" + } + + fn run_in(&self) -> tokio::time::Duration { + time_until(7, 00) + } + + async fn run(&self, ctx: ClientContext, client: GraphQLClient) -> anyhow::Result<()> { + mirror_new_updates(ctx, client).await + } +} + +pub async fn mirror_new_updates(ctx: ClientContext, client: GraphQLClient) -> anyhow::Result<()> { + let emails = tokio::task::spawn_blocking(fetch_inbox).await??; + if emails.is_empty() { + return Ok(()); + } + let member_data = client.fetch_member_data(Local::now().date_naive()).await?; + + let mut members = HashMap::new(); + for member in &member_data { + members.insert(member.email.trim().to_lowercase(), member); + } + + for email in emails { + let sender_email = email.from.trim().to_lowercase(); + + if let Some(member) = members.get(&sender_email) { + if member.track.is_none() || member.group_id.is_none() { + continue; + } + send_update( + &ctx, + member.name.clone(), + member.track.clone().unwrap(), + member.group_id.unwrap(), + email.body.clone(), + ) + .await?; + } + } + Ok(()) +} + +async fn send_update( + ctx: &ClientContext, + name: String, + track: String, + group: i32, + content: String, +) -> anyhow::Result<()> { + let channel_id = match (track.as_str(), group) { + ("Inductee", 1) => GROUP_ONE_STATUS_UPDATE_CHANNEL_ID, + ("Inductee", 2) => GROUP_TWO_STATUS_UPDATE_CHANNEL_ID, + ("Inductee", 3) => GROUP_THREE_STATUS_UPDATE_CHANNEL_ID, + ("Inductee", 4) => GROUP_FOUR_STATUS_UPDATE_CHANNEL_ID, + ("AI", _) => AI_STATUS_UPDATE_CHANNEL_ID, + ("Web", _) => WEB_STATUS_UPDATE_CHANNEL_ID, + ("Mobile", _) => MOBILE_STATUS_UPDATE_CHANNEL_ID, + ("Systems", _) => SYSTEMS_STATUS_UPDATE_CHANNEL_ID, + _ => STATUS_UPDATE_CHANNEL_ID, + }; + + let embed = CreateEmbed::new() + .title(format!("Status Update: {}", name)) + .description(content); + + let msg = CreateMessage::new().embed(embed); + + let channel = ChannelId::new(channel_id); + channel.send_message(ctx.http(), msg).await?; + + Ok(()) +} + +fn fetch_inbox() -> anyhow::Result> { + let domain = "imap.gmail.com"; + let client = imap::ClientBuilder::new(domain, 993) + .connect() + .context("Failed to connect to IMAP server")?; + + let email_id = std::env::var("EMAIL_ID").context("EMAIL_ID not found in env")?; + + let app_password = + std::env::var("APP_PASSWORD").context("APP_PASSWORD not found in the ENV")?; + + let mut session = client + .login(email_id, app_password) + .map_err(|e| e.0) + .context("Failed to authenticate with email client")?; + + session.select("INBOX").context("Failed to select INBOX")?; + + let ids = session + .search(format!("SUBJECT \"{}\"", subject())) + .context("Couldn't find any emails with subject: {subject()}")?; + + let mut emails = Vec::new(); + + for id in ids.iter() { + let msgs = session + .fetch(id.to_string(), "RFC822") + .context("Failed to fetch email with id: {id.to_string()}")?; + + for msg in msgs.iter() { + if let Some(body) = msg.body() as Option<&[u8]> { + let parsed = + mailparse::parse_mail(body).context("Couldn't parse the email body")?; + let from = parsed.headers.get_first_value("From").unwrap_or_default(); + + let clean_from = if let (Some(i1), Some(i2)) = (from.find('<'), from.find('>')) { + from[i1 + 1..i2].to_string() + } else { + from.trim().to_string() + }; + + let txt = extract_plain_text_body(&parsed) + .unwrap_or_else(|| String::from_utf8_lossy(body).to_string()); + + emails.push(EmailDetails { + from: clean_from, + body: strip_signature(&txt), + }); + } + } + } + session.logout().context("Failed to logout")?; + Ok(emails) +} + +// This function is defined incase updates are sent every 30 minutes +fn subject() -> String { + let now = Local::now().with_timezone(&Kolkata); + + let subject_date = if now.hour() < 7 || (now.hour() == 7 && now.minute() <= 5) { + now - Duration::days(1) + } else { + now + }; + + format!( + "Status Update [{:02}-{:02}-{:04}]", + subject_date.day(), + subject_date.month(), + subject_date.year() + ) +} + +fn extract_plain_text_body(parsed: &ParsedMail) -> Option { + if parsed.ctype.mimetype == "text/plain" { + return Some(String::from_utf8_lossy(&parsed.get_body_raw().unwrap()).to_string()); + } + for sub in &parsed.subparts { + if let Some(t) = extract_plain_text_body(sub) { + return Some(t); + } + } + for sub in &parsed.subparts { + if sub.ctype.mimetype == "text/html" { + let raw = sub.get_body_raw().unwrap(); + let html = String::from_utf8_lossy(&raw); + return Some(html2text::from_read(html.as_bytes(), usize::MAX)); + } + } + None +} + +fn strip_signature(text: &str) -> String { + let mut result = Vec::new(); + for line in text.lines() { + if line.trim() == "--" || line.trim().starts_with("On ") && line.contains(" wrote:") { + break; + } + result.push(line); + } + result.join("\n").trim().to_string() +} From 874b53ab4ebca9816eb8fbb7e118dcdb70cfe29b Mon Sep 17 00:00:00 2001 From: Hridesh MG Date: Sat, 7 Mar 2026 00:14:42 +0530 Subject: [PATCH 2/2] fix: rename ENV variables to follow bedrock conventions --- .env.sample | 4 ++-- src/tasks/status_update_mirror.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index 8961c88..e7ce847 100644 --- a/.env.sample +++ b/.env.sample @@ -4,5 +4,5 @@ OWNER_ID= DEBUG=true ENABLE_DEBUG_LIBRARIES=false AMD_API_KEY= -APP_PASSWORD= -EMAIL_ID= \ No newline at end of file +AMD_APP_PASSWORD= +AMD_EMAIL_ID= diff --git a/src/tasks/status_update_mirror.rs b/src/tasks/status_update_mirror.rs index f3cca43..997e698 100644 --- a/src/tasks/status_update_mirror.rs +++ b/src/tasks/status_update_mirror.rs @@ -129,10 +129,10 @@ fn fetch_inbox() -> anyhow::Result> { .connect() .context("Failed to connect to IMAP server")?; - let email_id = std::env::var("EMAIL_ID").context("EMAIL_ID not found in env")?; + let email_id = std::env::var("AMD_EMAIL_ID").context("EMAIL_ID not found in env")?; let app_password = - std::env::var("APP_PASSWORD").context("APP_PASSWORD not found in the ENV")?; + std::env::var("AMD_APP_PASSWORD").context("APP_PASSWORD not found in the ENV")?; let mut session = client .login(email_id, app_password)