From 08c440e1b11f957be788ce8c4b83a1494d39be58 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:05:10 +0100 Subject: [PATCH 1/8] src: Remove skipped WRs --- src/config.rs | 21 +++------------------ src/mail.rs | 22 +++++++++++----------- src/main.rs | 24 ++++++++++-------------- src/stats.rs | 4 ---- src/wr.rs | 4 ---- 5 files changed, 24 insertions(+), 51 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5631eb7..9499d5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,11 @@ use serde::Deserialize; -#[derive(Deserialize, Debug)] -pub struct Config { - // The mail configuration - pub mail: MailConfig, - // The statistics configuration - pub stats: StatsConfig, -} - #[derive(Deserialize, Debug)] pub struct MailConfig { // The login configuration - pub login: MailLogin, + pub server: MailLogin, // The fetch configuration - pub fetch: MailFetch, + pub query: MailQuery, } #[derive(Deserialize, Debug)] @@ -29,7 +21,7 @@ pub struct MailLogin { } #[derive(Deserialize, Debug, Clone)] -pub struct MailFetch { +pub struct MailQuery { // The mailboxes to fetch from the WRs you sent pub wr_mailboxes: Vec, // The mailboxes to fetch from the WR replies you received @@ -43,10 +35,3 @@ pub struct MailFetch { // The year to fetch the WRs from pub year: u32, } - -#[derive(Deserialize, Debug)] -pub struct StatsConfig { - // The number of holiday weeks you took this year, where you didn't had - // to write a WR, this includes sick days, vacation, etc. - pub num_holidays: u32, -} diff --git a/src/mail.rs b/src/mail.rs index 2215592..2b57e0f 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -8,7 +8,7 @@ use itertools::join; use log::{info, warn}; use std::str::from_utf8; -use crate::config::{MailConfig, MailFetch, MailLogin}; +use crate::config::{MailConfig, MailLogin, MailQuery}; use crate::error::{Result, WrError}; #[derive(Debug, Clone)] @@ -101,7 +101,7 @@ fn imap_login(login: &MailLogin) -> Result pub fn list_mailboxes(config: &MailConfig) -> Result<()> { // Login to the IMAP server - let mut imap_session = imap_login(&config.login)?; + let mut imap_session = imap_login(&config.server)?; // List all mailboxes let mailboxes = imap_session.list(Some(""), Some("*"))?; @@ -117,7 +117,7 @@ pub fn list_mailboxes(config: &MailConfig) -> Result<()> { pub fn fetch_inbox(config: &MailConfig) -> Result<()> { // Login to the IMAP server - let mut imap_session = imap_login(&config.login)?; + let mut imap_session = imap_login(&config.server)?; // Select the INBOX mailbox imap_session.select("INBOX")?; @@ -141,7 +141,7 @@ pub fn fetch_inbox(config: &MailConfig) -> Result<()> { Ok(()) } -fn build_imap_search_query(fetch: &MailFetch) -> Result { +fn build_imap_search_query(fetch: &MailQuery) -> Result { // Check that patterns is not empty if fetch.pattern.is_empty() { return Err(WrError::QueryError("No pattern specified".to_string())); @@ -174,17 +174,17 @@ fn build_imap_search_query(fetch: &MailFetch) -> Result { Ok(query) } -pub fn fetch_wrs(config: &MailConfig) -> Result> { +pub fn fetch_wrs(config: &MailConfig) -> Result> { // Login to the IMAP server - let mut imap_session = imap_login(&config.login)?; + let mut imap_session = imap_login(&config.server)?; // Search for messages that contain the pattern - let query = build_imap_search_query(&config.fetch)?; + let query = build_imap_search_query(&config.query)?; // List of WRs let mut wrs = Vec::new(); - for mailbox in config.fetch.wr_mailboxes.iter() { + for mailbox in config.query.wr_mailboxes.iter() { // Select the mailbox match imap_session.select(mailbox) { Ok(_) => {} @@ -233,9 +233,9 @@ pub fn fetch_wrs(config: &MailConfig) -> Result> { pub fn fetch_replies(config: &MailConfig) -> Result> { // Login to the IMAP server - let mut imap_session = imap_login(&config.login)?; + let mut imap_session = imap_login(&config.server)?; - let mut reply_fetch = config.fetch.clone(); + let mut reply_fetch = config.query.clone(); // Swap `from` and `to` in the fetch configuration std::mem::swap(&mut reply_fetch.from, &mut reply_fetch.to); @@ -245,7 +245,7 @@ pub fn fetch_replies(config: &MailConfig) -> Result> { // List of WRs let mut wr_replies = Vec::new(); - for mailbox in config.fetch.re_mailboxes.iter() { + for mailbox in config.query.re_mailboxes.iter() { // Select the mailbox match imap_session.select(mailbox) { Ok(_) => {} diff --git a/src/main.rs b/src/main.rs index b817cfe..3151d17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,10 +30,10 @@ async fn main() -> Result<()> { let config_contents = fs::read_to_string("config.toml") .map_err(|_| WrError::ConfigError("Could not read config file".to_string()))?; - let mut config: config::Config = toml::from_str(&config_contents) + let mut mail_config: config::MailConfig = toml::from_str(&config_contents) .map_err(|_| WrError::ConfigError("Could not parse config file".to_string()))?; - let username = match config.mail.login.username { + let username = match mail_config.server.username { Some(username) => Some(username), None => { eprint!("Username: "); @@ -43,28 +43,24 @@ async fn main() -> Result<()> { } }; - let password = match config.mail.login.password { + let password = match mail_config.server.password { Some(password) => Some(password), None => Some(rpassword::prompt_password("Password: ").unwrap()), }; - config.mail.login.username = username; - config.mail.login.password = password; + mail_config.server.username = username; + mail_config.server.password = password; let matches = cli().get_matches(); match matches.subcommand() { - Some(("mailboxes", _)) => mail::list_mailboxes(&config.mail), - Some(("fetch-inbox", _)) => mail::fetch_inbox(&config.mail), + Some(("mailboxes", _)) => mail::list_mailboxes(&mail_config), + Some(("fetch-inbox", _)) => mail::fetch_inbox(&mail_config), _ => { - let wrs = mail::fetch_wrs(&config.mail)?; - let replies = mail::fetch_replies(&config.mail)?; + let wrs = mail::fetch_wrs(&mail_config)?; + let replies = mail::fetch_replies(&mail_config)?; let merged_wrs = wr::merge_wrs(&wrs, &replies); - let stats = stats::Stats::from_wrs( - &merged_wrs, - config.mail.fetch.year, - config.stats.num_holidays, - ); + let stats = stats::Stats::from_wrs(&merged_wrs, mail_config.query.year); stats.write_to_file("shared/stats.json")?; let localhost = "127.0.0.1:8080"; let url = format!("http://{}/", localhost); diff --git a/src/stats.rs b/src/stats.rs index 571e0e1..5c4c1d0 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -19,8 +19,6 @@ pub struct Stats { pub num_replied_wrs: usize, // The ratio of WRs that were replied to pub ratio_replied_wrs: f64, - // The number of skipped WRs - pub num_skipped_wrs: usize, // The average delay of the WRs pub avg_wr_delay: f64, // The average delay of the replied WRs @@ -38,13 +36,11 @@ pub struct Stats { } impl Stats { - pub fn from_wrs(wrs: &WRs, year: u32, num_holidays: u32) -> Self { Stats { year, num_wrs: wrs.num_wrs(), num_replied_wrs: wrs.num_replied_wrs(), ratio_replied_wrs: wrs.ratio_replied_wrs(), - num_skipped_wrs: wrs.num_skipped_wrs(num_holidays), avg_wr_delay: wrs.avg_wr_delay(), avg_reply_delay: wrs.avg_reply_delay(), weekday_wr_histogram: wrs.weekday_wr_histogram(), diff --git a/src/wr.rs b/src/wr.rs index a0c1cc1..2179889 100644 --- a/src/wr.rs +++ b/src/wr.rs @@ -97,10 +97,6 @@ impl WRs { self.wrs.iter().filter(|wr| wr.reply.is_some()).count() } - pub fn num_skipped_wrs(&self, num_holidays: u32) -> usize { - let num_holidays = num_holidays as usize; - let num_wrs = self.num_wrs(); - 52 - num_holidays - num_wrs } pub fn ratio_replied_wrs(&self) -> f64 { From 398469f718e3c78148580c62f15951384e922ab0 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:06:16 +0100 Subject: [PATCH 2/8] feat: Parse the mail body to calculate the number of words --- Cargo.toml | 1 + src/error.rs | 13 ++++++++-- src/mail.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++----- src/stats.rs | 4 +++ src/wr.rs | 62 ++++++++++++++++++++++++-------------------- 5 files changed, 117 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b45df0..9b9fd48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ imap = "3.0.0-alpha.12" imap-proto = "0.16.3" itertools = "0.12.0" log = "0.4.20" +mailparse = "0.14.1" native-tls = "0.2.11" open = "5.0.1" pretty_env_logger = "0.5.0" diff --git a/src/error.rs b/src/error.rs index 86c1ab5..92325ab 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,7 +7,7 @@ pub enum WrError { // IO Error IoError(std::io::Error), // Error from the IMAP crate - ImapError(imap::error::Error), + ImapError(String), // Query Error QueryError(String), // Config Error @@ -16,6 +16,8 @@ pub enum WrError { SerializationError(String), // Server Error ServerError(String), + // Mail parsing error + MailParseError(String), } impl std::fmt::Display for WrError { @@ -27,6 +29,7 @@ impl std::fmt::Display for WrError { WrError::ConfigError(e) => write!(f, "Config error: {}", e), WrError::SerializationError(e) => write!(f, "Serialization error: {}", e), WrError::ServerError(e) => write!(f, "Server error: {}", e), + WrError::MailParseError(e) => write!(f, "Mail parse error: {}", e), } } } @@ -39,7 +42,7 @@ impl From for WrError { impl From for WrError { fn from(error: imap::error::Error) -> Self { - WrError::ImapError(error) + WrError::ImapError(error.to_string()) } } @@ -49,4 +52,10 @@ impl From for WrError { } } +impl From for WrError { + fn from(error: mailparse::MailParseError) -> Self { + WrError::MailParseError(error.to_string()) + } +} + impl std::error::Error for WrError {} diff --git a/src/mail.rs b/src/mail.rs index 2b57e0f..b51a061 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -5,6 +5,7 @@ extern crate native_tls; use chrono::{DateTime, FixedOffset}; use imap::ImapConnection; use itertools::join; +use log::debug; use log::{info, warn}; use std::str::from_utf8; @@ -84,6 +85,13 @@ impl Envelope { } } +#[derive(Debug, Clone)] +pub struct Mail { + pub id: u32, + pub env: Envelope, + pub body: Option, +} + fn imap_login(login: &MailLogin) -> Result>> { let domain = login.server.as_str(); let port = login.port; @@ -174,6 +182,25 @@ fn build_imap_search_query(fetch: &MailQuery) -> Result { Ok(query) } +fn get_plain_text(mail: &mailparse::ParsedMail) -> Result { + if mail.subparts.is_empty() && mail.ctype.mimetype == "text/plain" { + let body = mail + .get_body() + .map_err(|_| WrError::MailParseError("Failed to parse mail body".to_string()))?; + return Ok(body); + } + let mut body_str: String = String::new(); + for part in mail.subparts.iter() { + let body = part.get_body()?; + if part.ctype.mimetype == "text/plain" { + body_str.push_str(&body); + } else { + body_str.push_str(&get_plain_text(part)?); + } + } + Ok(body_str) +} + pub fn fetch_wrs(config: &MailConfig) -> Result> { // Login to the IMAP server let mut imap_session = imap_login(&config.server)?; @@ -199,8 +226,11 @@ pub fn fetch_wrs(config: &MailConfig) -> Result> { let mut sequence_set: Vec<_> = sequence_set.into_iter().collect(); sequence_set.sort(); let sequence_set: String = join(sequence_set.into_iter().map(|s| s.to_string()), ","); + // Fetch the messages + info!("Fetching envelope of {} potential WRs", sequence_set.len()); let messages = imap_session.fetch(sequence_set, "ENVELOPE")?; + debug!("Got {} messages", messages.len()); // Print the subjects of the messages for message in messages.iter() { @@ -210,28 +240,53 @@ pub fn fetch_wrs(config: &MailConfig) -> Result> { match envelope.in_reply_to { None => { let env = Envelope::from_imap_envelope(envelope); - wrs.push(env); + debug!("Found WR with subject: {}", env.subject); + wrs.push(Mail { + id: message.message, + env, + body: None, + }); } Some(_) => { let subject = from_utf8(envelope.subject.as_ref().unwrap().as_ref()) .expect("No subject in the envelope"); if reply_pattern.iter().any(|&s| subject.contains(s)) { + debug!("Skipping reply with subject: {}", subject); continue; } let env = Envelope::from_imap_envelope(envelope); - wrs.push(env); + wrs.push(Mail { + id: message.message, + env, + body: None, + }); } }; } - } - info!("Found {} WRs", wrs.len()); + info!("Found {} WRs", wrs.len()); + + // Construct a new sequence set of all the WRs + let sequence_set: Vec = wrs.iter().map(|m: &Mail| m.id).collect(); + let sequence_set: String = join(sequence_set.into_iter().map(|s| s.to_string()), ","); + + // Fetch the bodies of the WRs + info!("Fetching bodies of {} WRs", wrs.len()); + let messages = imap_session.fetch(sequence_set, "BODY[]")?; + + // Add the text of the body to the WRs + for (message, wr) in messages.iter().zip(wrs.iter_mut()) { + let body = message.body().unwrap(); + let parsed_mail = mailparse::parse_mail(body)?; + wr.body = get_plain_text(&parsed_mail).ok(); // If an error occurs, just skip the body + } + } imap_session.logout()?; Ok(wrs) } -pub fn fetch_replies(config: &MailConfig) -> Result> { +pub fn fetch_replies(config: &MailConfig) -> Result> { // Login to the IMAP server let mut imap_session = imap_login(&config.server)?; @@ -262,6 +317,7 @@ pub fn fetch_replies(config: &MailConfig) -> Result> { let sequence_set: String = join(sequence_set.into_iter().map(|s| s.to_string()), ","); // Fetch the messages + info!("Fetching {} potential Replies", sequence_set.len()); let messages = imap_session.fetch(sequence_set, "ENVELOPE")?; // Print the subjects of the messages @@ -271,7 +327,11 @@ pub fn fetch_replies(config: &MailConfig) -> Result> { match envelope.in_reply_to { Some(_) => { let env = Envelope::from_imap_envelope(envelope); - wr_replies.push(env); + wr_replies.push(Mail { + id: message.message, + env, + body: None, + }); } None => continue, }; diff --git a/src/stats.rs b/src/stats.rs index 5c4c1d0..fe90bce 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -19,6 +19,8 @@ pub struct Stats { pub num_replied_wrs: usize, // The ratio of WRs that were replied to pub ratio_replied_wrs: f64, + // The number of words written + pub num_words: usize, // The average delay of the WRs pub avg_wr_delay: f64, // The average delay of the replied WRs @@ -36,11 +38,13 @@ pub struct Stats { } impl Stats { + pub fn from_wrs(wrs: &WRs, year: u32) -> Self { Stats { year, num_wrs: wrs.num_wrs(), num_replied_wrs: wrs.num_replied_wrs(), ratio_replied_wrs: wrs.ratio_replied_wrs(), + num_words: wrs.num_words(), avg_wr_delay: wrs.avg_wr_delay(), avg_reply_delay: wrs.avg_reply_delay(), weekday_wr_histogram: wrs.weekday_wr_histogram(), diff --git a/src/wr.rs b/src/wr.rs index 2179889..fc05fdc 100644 --- a/src/wr.rs +++ b/src/wr.rs @@ -2,49 +2,48 @@ use chrono::{Datelike, Timelike}; use log::info; use std::collections::HashMap; -use crate::mail::Envelope; - -pub fn merge_wrs(wr: &[Envelope], wr_re: &[Envelope]) -> WRs { - let mut wrs = WRs::new(); - - for w in wr.iter() { - let mut wr = WR::new(w.clone(), None); - for r in wr_re.iter() { - if let Some(message_id) = wr.sent.message_id.as_ref() { - if let Some(in_reply_to) = r.in_reply_to.as_ref() { +use crate::mail::Mail; + +pub fn merge_wrs(wrs: &[Mail], wrs_re: &[Mail]) -> WRs { + let mut merged_wrs = WRs::new(); + for wr_mail in wrs.iter() { + let mut wr = WR::new(wr_mail.clone(), None); + for re_mail in wrs_re.iter() { + if let Some(message_id) = wr_mail.env.message_id.as_ref() { + if let Some(in_reply_to) = re_mail.env.in_reply_to.as_ref() { if message_id.eq(in_reply_to) { - wr.reply = Some(r.clone()); + wr.reply = Some(re_mail.clone()); break; } } } } - wrs.wrs.push(wr); + merged_wrs.wrs.push(wr); } info!( - "Found {} Replies to {} WRs", - wrs.num_replied_wrs(), - wrs.num_wrs() + "Merged {} Replies with {} WRs", + merged_wrs.num_replied_wrs(), + merged_wrs.num_wrs() ); - wrs + merged_wrs } #[derive(Debug)] pub struct WR { // The Envelope of the WR that was sent - pub sent: Envelope, + pub sent: Mail, // The Envelope of the WR reply that was received, if any - pub reply: Option, + pub reply: Option, } impl WR { - pub fn new(sent: Envelope, reply: Option) -> Self { + pub fn new(sent: Mail, reply: Option) -> Self { WR { sent, reply } } pub fn wr_delay(&self) -> i64 { - let weekday = self.sent.date.weekday(); + let weekday = self.sent.env.date.weekday(); let days_since_friday = (weekday.num_days_from_monday() + 2) % 7; days_since_friday as i64 } @@ -52,14 +51,21 @@ impl WR { pub fn reply_delay(&self) -> Option { match self.reply { Some(ref reply) => { - let sent_date = self.sent.date; - let reply_date = reply.date; + let sent_date = self.sent.env.date; + let reply_date = reply.env.date; let duration = reply_date.signed_duration_since(sent_date); Some(duration.num_days()) } None => None, } } + + pub fn num_words(&self) -> usize { + match self.sent.body { + Some(ref body) => body.split_whitespace().count(), + None => 0, + } + } } #[derive(Debug, Default)] @@ -97,6 +103,8 @@ impl WRs { self.wrs.iter().filter(|wr| wr.reply.is_some()).count() } + pub fn num_words(&self) -> usize { + self.wrs.iter().map(|wr| wr.num_words()).sum() } pub fn ratio_replied_wrs(&self) -> f64 { @@ -124,7 +132,7 @@ impl WRs { hist.insert(day, 0); } for wr in self.wrs.iter() { - let weekday = wr.sent.date.weekday(); + let weekday = wr.sent.env.date.weekday(); hist.entry(weekday as u32).and_modify(|e| *e += 1); } hist @@ -139,7 +147,7 @@ impl WRs { for wr in self.wrs.iter() { match wr.reply { Some(_) => { - let weekday = wr.sent.date.weekday(); + let weekday = wr.sent.env.date.weekday(); hist.entry(weekday as u32).and_modify(|e| *e += 1); } None => continue, @@ -155,7 +163,7 @@ impl WRs { hist.insert(hour, 0); } for wr in self.wrs.iter() { - let hour = wr.sent.date.hour(); + let hour = wr.sent.env.date.hour(); hist.entry(hour).and_modify(|e| *e += 1); } hist @@ -170,7 +178,7 @@ impl WRs { for wr in self.wrs.iter() { match wr.reply { Some(_) => { - let hour = wr.sent.date.hour(); + let hour = wr.sent.env.date.hour(); hist.entry(hour).and_modify(|e| *e += 1); } None => continue, @@ -183,7 +191,7 @@ impl WRs { let mut hist = HashMap::new(); for wr in self.wrs.iter() { - match wr.sent.cc { + match wr.sent.env.cc { Some(ref cc) => { for addr in cc.iter() { if let Some(user) = &addr.user { From c921ae0aa4364cb4f726f808ddffd63b45d0d944 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:06:38 +0100 Subject: [PATCH 3/8] web: Replace num-skipped-wrs with num-words --- web/css/styles.css | 2 +- web/index.html | 9 +++++---- web/js/main.js | 16 ++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/web/css/styles.css b/web/css/styles.css index 198a5e9..5c49828 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -129,7 +129,7 @@ body { justify-content: center; } -#num-skipped-wrs-tile { +#num-words-tile { grid-column: 1 / 3; grid-row: 2 / 3; display: flex; diff --git a/web/index.html b/web/index.html index 8dba6ef..06006b2 100644 --- a/web/index.html +++ b/web/index.html @@ -13,11 +13,12 @@
-
written...
+
written
-
-
-
...skipped
+
+
containing
+
+
words
diff --git a/web/js/main.js b/web/js/main.js index 0ecda94..2cdf2d2 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -387,7 +387,7 @@ function switchPalette(year) { document.addEventListener('DOMContentLoaded', function() { const yearId = 'wrapped-year'; const numWrsWrittenId = 'num-wrs-written'; - const numWrsSkippedId = 'num-wrs-skipped'; + const numWordsId = 'num-words'; const progressCircleId = 'progress-circle'; const ratioTextOverlayId = 'ratio-text-overlay'; const delayOfReplyId = 'delay-of-reply'; @@ -398,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() { let weekdayData; let timeofdayData; let numWrsWritten; - let numWrsSkipped; + let numWords; let ccData; const numWrsWrittenContainer= document.getElementById(numWrsWrittenId); @@ -407,10 +407,10 @@ document.addEventListener('DOMContentLoaded', function() { numWrsWrittenContainer.textContent = numWrsText; } - const numWrsSkippedContainer = document.getElementById(numWrsSkippedId); - function updateNumWrsSkipped(numWrs) { - const numWrsText = numWrs + " WRs" - numWrsSkippedContainer.textContent = numWrsText; + const numWordsContainer = document.getElementById(numWordsId); + function updateNumWords(NumWords) { + const NumWordsText = parseInt(parseInt(NumWords) / 1000) + "k" + numWordsContainer.textContent = NumWordsText; } function resizeProgressCircleChart() { @@ -455,14 +455,14 @@ document.addEventListener('DOMContentLoaded', function() { switchPalette(year); ratioRepliedWRs = data.ratio_replied_wrs; numWrsWritten = data.num_wrs; - numWrsSkipped = data.num_skipped_wrs; + numWords = data.num_words; delayDays = data.avg_reply_delay; weekdayData = data.weekday_wr_histogram; timeofdayData = data.hour_reply_histogram; ccData = data.cc_histogram; updateYear(year); updateNumWrsWritten(numWrsWritten); - updateNumWrsSkipped(numWrsSkipped); + updateNumWords(numWords); updateTextOverlay(ratioRepliedWRs); updateDelay(delayDays); updateCCList(ccData); From 39447a9814359081c79c6a7f892c669a82e4ff8f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:06:47 +0100 Subject: [PATCH 4/8] doc: Update README --- README.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b6cf188..e6af733 100644 --- a/README.md +++ b/README.md @@ -37,20 +37,23 @@ The configuration file is written in [TOML](https://toml.io). To login to your E server = "my.mail.server" # The port to connect to port = 993 -# (Optional) The username to use for authentication -# username = "my_username" -# (Optional) The password to use for authentication -# password = "my_password" ``` The configure which E-Mails to search for and fetch, you need to provide the following information: ```toml [mail.fetch] -# The mailboxes to fetch from the WRs you sent, you can also run `cargo run mailboxes` to get a list of all mailboxes you have. +# The mailboxes to fetch from the WRs you sent, +# you can also run `cargo run mailboxes` to get a list of all mailboxes you have. wr_mailboxes = ["Sent", "Sent Messages"] -# The mailboxes to fetch from the WR replies you received. Usually you only need to fetch from the INBOX. However, if you have a rule that moves the WR replies to a different mailbox, you need to add it here. +# The mailboxes to fetch from the WR replies you received. +# Usually you only need to fetch from the INBOX. However, +# if you have a rule that moves the WR replies to a different mailbox, +# you need to add it here. re_mailboxes = ["INBOX"] -# The pattern to match the WR subject you sent. This will match all subjects that contain the strings "WR" OR "Weekly Report". This means that your Subject needs to be consistent over the years. Currently, you can only match at most two patterns (this is a limitation of the IMAP search query). +# The pattern to match the WR subject you sent. +# This will match all subjects that contain the strings "WR" OR "Weekly Report". +# This means that your Subject needs to be consistent over the years. +# Currently, you can only match at most two patterns (this is a limitation of the IMAP search query). pattern = ["WR", "Weekly Report"] # From which mail address you sent the WRs from = "my_username@my.mail.server" @@ -60,14 +63,6 @@ to = "theboss@my.mail.server" year = 2023 ``` -Lastly, you have to configure a few things how the statistics are generated: - -```toml -[stats] -# The number of holiday weeks you were not working. This includes holidays, sick days, etc. -num_holidays = 5 -``` - ## How it works The script works by connecting to your E-Mail account using IMAP. It then searches for all E-Mails that match the given criteria. For instance it creates an IMAP search query that looks like this: @@ -76,7 +71,7 @@ The script works by connecting to your E-Mail account using IMAP. It then search FROM "my_username@my.mail.server" TO "theboss@my.mail.server" SUBJECT "WR" OR SUBJECT "Weekly Report" SINCE 01-Jan-2023 BEFORE 31-Dec-2023 ``` -which will return a sequence of E-Mail IDs. The script then fetches only the header (or `ENVELOPE` in IMAP terms) of each E-Mail, which contains information such as the date, the sender, the recipient, etc. The content of the mail is not fetched at all. The script then parses the date and sender information to create a list of WRs. +which will return a sequence of E-Mail IDs. The script then fetches first the header (or `ENVELOPE` in IMAP terms) of each E-Mail, which contains information such as the date, the sender, the recipient, etc. In a second step, the content (or `BODY` in IMAP terms) is fetched and merged with the header to create a list of WRs. The replies are fetched in a similar way, but the other way around: From 20e307deead7851bdb96225c017d554e8cd784ea Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:09:58 +0100 Subject: [PATCH 5/8] config: Update example config file --- config.toml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/config.toml b/config.toml index e3cf7f9..f532b99 100644 --- a/config.toml +++ b/config.toml @@ -3,17 +3,23 @@ server = "my.mail.server" # The port to connect to port = 993 -# (Optional) The username to use for authentication -# username = "my_username" -# (Optional) The password to use for authentication -# password = "my_password" [mail.fetch] -# The mailboxes to fetch from the WRs you sent, you can also run `cargo run mailboxes` to get a list of all mailboxes you have. -wr_mailboxes = ["Sent", "Sent Messages"] -# The mailboxes to fetch from the WR replies you received. Usually you only need to fetch from the INBOX. However, if you have a rule that moves the WR replies to a different mailbox, you need to add it here. +# The mailboxes to fetch from the WRs you sent, +# you can also run `cargo run mailboxes` to get +# a list of all mailboxes you have. +wr_mailboxes = ["Sent"] +# The mailboxes to fetch from the WR replies you received. +# Usually you only need to fetch from the INBOX. However, +# if you have a rule that moves the WR replies to a +# different mailbox, you need to add it here. re_mailboxes = ["INBOX"] -# The pattern to match the WR subject you sent. This will match all subjects that contain the strings "WR" OR "Weekly Report". This means that your Subject needs to be consistent over the years. Currently, you can only match at most two patterns (this is a limitation of the IMAP search query). +# The pattern to match the WR subject you sent. +# This will match all subjects that contain the +# strings "WR" OR "Weekly Report". This means that +# your Subject needs to be consistent over the years. +# Currently, you can only match at most two patterns +# (this is a limitation of the IMAP search query). pattern = ["WR", "Weekly Report"] # From which mail address you sent the WRs from = "my_username@my.mail.server" @@ -21,7 +27,3 @@ from = "my_username@my.mail.server" to = "theboss@my.mail.server" # The year to fetch the WRs from year = 2023 - -[stats] -# The number of holiday weeks you were not working. This includes holidays, sick days, etc. -num_holidays = 5 From c9bf73e28557dfd2da330e932725afd36af62c38 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:13:20 +0100 Subject: [PATCH 6/8] ci: Simplify CI workflow to only run one job --- .github/workflows/ci.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a43f27..50d6e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,23 +6,13 @@ env: RUSTFLAGS: "-Dwarnings" jobs: - clippy_check: + checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Clippy run: cargo clippy --all-targets --all-features - - format_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Run Rustfmt + - name: Run Rustfmt (run `cargo fmt --all` to fix) run: cargo fmt --all -- --check - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - name: Build run: cargo build --all-targets --all-features From 46c377aff335aa75ab5d4ac148948f523edf96d1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:15:22 +0100 Subject: [PATCH 7/8] ci: Build before linting and formatting --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50d6e9d..e963d95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Build + run: cargo build --all-targets --all-features - name: Run Clippy run: cargo clippy --all-targets --all-features - name: Run Rustfmt (run `cargo fmt --all` to fix) run: cargo fmt --all -- --check - - name: Build - run: cargo build --all-targets --all-features From 06a5d97c5e77b8035cc84da7082908a5a444d180 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 28 Jan 2024 21:18:52 +0100 Subject: [PATCH 8/8] ci: Don't lint dependencies --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e963d95..b535de5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: cargo build --all-targets --all-features + run: cargo build - name: Run Clippy - run: cargo clippy --all-targets --all-features + run: cargo clippy --no-deps - name: Run Rustfmt (run `cargo fmt --all` to fix) run: cargo fmt --all -- --check