diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0a43f27 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +on: [push, pull_request] +name: ci + +# Make sure CI fails on all warnings, including Clippy lints +env: + RUSTFLAGS: "-Dwarnings" + +jobs: + clippy_check: + 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 + run: cargo fmt --all -- --check + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --all-targets --all-features diff --git a/src/config.rs b/src/config.rs index 83831a1..5631eb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,6 @@ use serde::Deserialize; -#[derive(Deserialize)] -#[derive(Debug)] +#[derive(Deserialize, Debug)] pub struct Config { // The mail configuration pub mail: MailConfig, @@ -9,8 +8,7 @@ pub struct Config { pub stats: StatsConfig, } -#[derive(Deserialize)] -#[derive(Debug)] +#[derive(Deserialize, Debug)] pub struct MailConfig { // The login configuration pub login: MailLogin, @@ -18,8 +16,7 @@ pub struct MailConfig { pub fetch: MailFetch, } -#[derive(Deserialize)] -#[derive(Debug)] +#[derive(Deserialize, Debug)] pub struct MailLogin { // The IMAP server to connect to pub server: String, @@ -31,9 +28,7 @@ pub struct MailLogin { pub password: Option, } -#[derive(Deserialize)] -#[derive(Debug)] -#[derive(Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct MailFetch { // The mailboxes to fetch from the WRs you sent pub wr_mailboxes: Vec, @@ -49,8 +44,7 @@ pub struct MailFetch { pub year: u32, } -#[derive(Deserialize)] -#[derive(Debug)] +#[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. diff --git a/src/mail.rs b/src/mail.rs index 449c10c..2215592 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,36 +1,48 @@ +extern crate chrono; extern crate imap; extern crate native_tls; -extern crate chrono; +use chrono::{DateTime, FixedOffset}; use imap::ImapConnection; use itertools::join; -use chrono::{DateTime, FixedOffset}; -use std::str::from_utf8; use log::{info, warn}; +use std::str::from_utf8; -use crate::config::{MailConfig, MailLogin, MailFetch}; +use crate::config::{MailConfig, MailFetch, MailLogin}; use crate::error::{Result, WrError}; #[derive(Debug, Clone)] pub struct Address { pub name: Option, pub user: Option, - pub email: Option + pub email: Option, } impl Address { pub fn from_imap_address(addr: &imap_proto::types::Address) -> Self { Address { - name: addr.name.as_ref().map(|s| String::from_utf8_lossy(s).to_string()), - user: addr.mailbox.as_ref().map(|s| String::from_utf8_lossy(s).to_string()), + name: addr + .name + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()), + user: addr + .mailbox + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()), email: { - let host = addr.host.as_ref().map(|s| String::from_utf8_lossy(s).to_string()); - let mailbox = addr.mailbox.as_ref().map(|s| String::from_utf8_lossy(s).to_string()); + let host = addr + .host + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()); + let mailbox = addr + .mailbox + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()); match (mailbox, host) { (Some(mailbox), Some(host)) => Some(format!("{}@{}", mailbox, host)), _ => None, } - } + }, } } } @@ -48,21 +60,31 @@ impl Envelope { pub fn from_imap_envelope(envelope: &imap_proto::types::Envelope) -> Self { Envelope { date: { - let date_str = envelope.date.as_ref().map(|s| String::from_utf8_lossy(s).to_string()); + let date_str = envelope + .date + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()); DateTime::parse_from_rfc2822(&date_str.unwrap()).unwrap() }, subject: String::from_utf8_lossy(envelope.subject.as_ref().unwrap()).to_string(), - cc: envelope.cc.as_ref().map(|cc| cc.iter().map(|addr| Address::from_imap_address(addr)).collect()), - in_reply_to: envelope.in_reply_to.as_ref().map(|s| String::from_utf8_lossy(s).to_string()), - message_id: envelope.message_id.as_ref().map(|s| String::from_utf8_lossy(s).to_string()), + cc: envelope.cc.as_ref().map(|cc| { + cc.iter() + .map(|addr| Address::from_imap_address(addr)) + .collect() + }), + in_reply_to: envelope + .in_reply_to + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()), + message_id: envelope + .message_id + .as_ref() + .map(|s| String::from_utf8_lossy(s).to_string()), } } } -fn imap_login( - login: &MailLogin, -) -> Result>> { - +fn imap_login(login: &MailLogin) -> Result>> { let domain = login.server.as_str(); let port = login.port; let username = login.username.clone().unwrap(); @@ -72,17 +94,12 @@ fn imap_login( let client = imap::ClientBuilder::new(domain, port).connect()?; // Login to the IMAP server - let imap_session = client - .login(username, password) - .map_err(|e| e.0)?; + let imap_session = client.login(username, password).map_err(|e| e.0)?; Ok(imap_session) } -pub fn list_mailboxes( - config: &MailConfig, -) -> Result<()> { - +pub fn list_mailboxes(config: &MailConfig) -> Result<()> { // Login to the IMAP server let mut imap_session = imap_login(&config.login)?; @@ -98,10 +115,7 @@ pub fn list_mailboxes( Ok(()) } -pub fn fetch_inbox( - config: &MailConfig, -) -> Result<()> { - +pub fn fetch_inbox(config: &MailConfig) -> Result<()> { // Login to the IMAP server let mut imap_session = imap_login(&config.login)?; @@ -127,10 +141,7 @@ pub fn fetch_inbox( Ok(()) } -fn build_imap_search_query( - fetch: &MailFetch -) -> Result { - +fn build_imap_search_query(fetch: &MailFetch) -> Result { // Check that patterns is not empty if fetch.pattern.is_empty() { return Err(WrError::QueryError("No pattern specified".to_string())); @@ -138,13 +149,18 @@ fn build_imap_search_query( // Check that patterns has at most two elements if fetch.pattern.len() > 2 { - return Err(WrError::QueryError("IMAP search query supports a maximum of two patterns".to_string())); + return Err(WrError::QueryError( + "IMAP search query supports a maximum of two patterns".to_string(), + )); } // Format the subject of the query let mut query = match fetch.pattern.len() { 1 => format!("SUBJECT \"{}\"", fetch.pattern[0]), - 2 => format!("SUBJECT \"{}\" OR SUBJECT \"{}\"", fetch.pattern[0], fetch.pattern[1]), + 2 => format!( + "SUBJECT \"{}\" OR SUBJECT \"{}\"", + fetch.pattern[0], fetch.pattern[1] + ), _ => unreachable!(), }; @@ -158,10 +174,7 @@ fn build_imap_search_query( 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)?; @@ -174,11 +187,11 @@ pub fn fetch_wrs( for mailbox in config.fetch.wr_mailboxes.iter() { // Select the mailbox match imap_session.select(mailbox) { - Ok(_) => {}, + Ok(_) => {} Err(e) => { warn!("Could not select mailbox {}: {}", mailbox, e); continue; - }, + } } // Search for messages that contain the pattern @@ -192,21 +205,22 @@ pub fn fetch_wrs( // Print the subjects of the messages for message in messages.iter() { let envelope = message.envelope().unwrap(); - let reply_pattern= vec!["Re:", "RE:", "Aw:", "AW:"]; + let reply_pattern = ["Re:", "RE:", "Aw:", "AW:"]; match envelope.in_reply_to { None => { let env = Envelope::from_imap_envelope(envelope); wrs.push(env); - }, + } Some(_) => { - let subject = from_utf8(envelope.subject.as_ref().unwrap().as_ref()).expect("No subject in the envelope"); + 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)) { continue; } let env = Envelope::from_imap_envelope(envelope); wrs.push(env); - }, + } }; } } @@ -217,14 +231,10 @@ pub fn fetch_wrs( 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.login)?; - let mut reply_fetch = config.fetch.clone(); // Swap `from` and `to` in the fetch configuration std::mem::swap(&mut reply_fetch.from, &mut reply_fetch.to); @@ -238,11 +248,11 @@ pub fn fetch_replies( for mailbox in config.fetch.re_mailboxes.iter() { // Select the mailbox match imap_session.select(mailbox) { - Ok(_) => {}, + Ok(_) => {} Err(e) => { warn!("Could not select mailbox {}: {}", mailbox, e); continue; - }, + } } // Search for messages that contain the pattern @@ -262,7 +272,7 @@ pub fn fetch_replies( Some(_) => { let env = Envelope::from_imap_envelope(envelope); wr_replies.push(env); - }, + } None => continue, }; } diff --git a/src/main.rs b/src/main.rs index 0b18008..b817cfe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,14 @@ -use std::fs; use std::env; +use std::fs; use clap::Command; -use rpassword; -use pretty_env_logger; pub mod config; pub mod error; pub mod mail; -pub mod wr; -pub mod stats; pub mod server; +pub mod stats; +pub mod wr; use error::{Result, WrError}; @@ -18,27 +16,14 @@ fn cli() -> Command { Command::new("WRapped") .about("Wrapped but for Weekly Reports") .allow_external_subcommands(true) - .subcommand( - Command::new("mailboxes") - .about("List mailboxes") - ) - .subcommand( - Command::new("fetch-inbox") - .about("Fetch the first mail in the inbox") - ) - .subcommand( - Command::new("fetch-wrs") - .about("Fetch all WRs") - ) - .subcommand( - Command::new("fetch-replies") - .about("Fetch all the replies of the WRs") - ) + .subcommand(Command::new("mailboxes").about("List mailboxes")) + .subcommand(Command::new("fetch-inbox").about("Fetch the first mail in the inbox")) + .subcommand(Command::new("fetch-wrs").about("Fetch all WRs")) + .subcommand(Command::new("fetch-replies").about("Fetch all the replies of the WRs")) } #[actix_web::main] async fn main() -> Result<()> { - env::set_var("RUST_LOG", "actix_server=warn,info"); pretty_env_logger::init(); @@ -60,7 +45,7 @@ async fn main() -> Result<()> { let password = match config.mail.login.password { Some(password) => Some(password), - None => Some(rpassword::prompt_password("Password: ").unwrap()) + None => Some(rpassword::prompt_password("Password: ").unwrap()), }; config.mail.login.username = username; @@ -75,14 +60,18 @@ async fn main() -> Result<()> { let wrs = mail::fetch_wrs(&config.mail)?; let replies = mail::fetch_replies(&config.mail)?; 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, + config.mail.fetch.year, + config.stats.num_holidays, + ); stats.write_to_file("shared/stats.json")?; let localhost = "127.0.0.1:8080"; let url = format!("http://{}/", localhost); server::open_browser(&url); - server::run_server(&localhost).await?; + server::run_server(localhost).await?; Ok(()) - }, + } }?; Ok(()) diff --git a/src/server.rs b/src/server.rs index 4cf7acc..6cb5906 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,7 +4,7 @@ use std::time::Duration; use actix_files as fs; use actix_web::{App, HttpServer}; -use log::{info, error}; +use log::{error, info}; pub fn open_browser(server: &str) { thread::sleep(Duration::from_secs(1)); @@ -14,7 +14,6 @@ pub fn open_browser(server: &str) { } pub async fn run_server(server: &str) -> std::io::Result<()> { - info!("Starting the web server at {}", server); HttpServer::new(move || { App::new() diff --git a/src/stats.rs b/src/stats.rs index b1134ba..571e0e1 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -40,7 +40,7 @@ pub struct Stats { impl Stats { pub fn from_wrs(wrs: &WRs, year: u32, num_holidays: u32) -> Self { Stats { - year: year, + year, num_wrs: wrs.num_wrs(), num_replied_wrs: wrs.num_replied_wrs(), ratio_replied_wrs: wrs.ratio_replied_wrs(), diff --git a/src/wr.rs b/src/wr.rs index 4fe243b..a0c1cc1 100644 --- a/src/wr.rs +++ b/src/wr.rs @@ -1,34 +1,33 @@ -use std::collections::HashMap; use chrono::{Datelike, Timelike}; use log::info; +use std::collections::HashMap; use crate::mail::Envelope; - -pub fn merge_wrs( - wr: &Vec, - wr_re: &Vec, -) -> 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() { - if message_id.eq(in_reply_to) { - wr.reply = Some(r.clone()); - break; - } +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() { + if message_id.eq(in_reply_to) { + wr.reply = Some(r.clone()); + break; } } } - wrs.wrs.push(wr); } + wrs.wrs.push(wr); + } - info!("Found {} Replies to {} WRs", wrs.num_replied_wrs(), wrs.num_wrs()); - wrs + info!( + "Found {} Replies to {} WRs", + wrs.num_replied_wrs(), + wrs.num_wrs() + ); + wrs } #[derive(Debug)] @@ -63,7 +62,7 @@ impl WR { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct WRs { // All the WRs that were sent and received pub wrs: Vec, @@ -71,9 +70,7 @@ pub struct WRs { impl WRs { pub fn new() -> Self { - WRs { - wrs: Vec::new(), - } + WRs { wrs: Vec::new() } } pub fn push(&mut self, wr: WR) { @@ -128,7 +125,7 @@ impl WRs { let mut hist = HashMap::new(); for day in 0..7 { - hist.insert(day, 0 as u32); + hist.insert(day, 0); } for wr in self.wrs.iter() { let weekday = wr.sent.date.weekday(); @@ -141,14 +138,14 @@ impl WRs { let mut hist = HashMap::new(); for day in 0..7 { - hist.insert(day, 0 as u32); + hist.insert(day, 0); } for wr in self.wrs.iter() { match wr.reply { Some(_) => { let weekday = wr.sent.date.weekday(); hist.entry(weekday as u32).and_modify(|e| *e += 1); - }, + } None => continue, }; } @@ -159,7 +156,7 @@ impl WRs { let mut hist = HashMap::new(); for hour in 0..24 { - hist.insert(hour, 0 as u32); + hist.insert(hour, 0); } for wr in self.wrs.iter() { let hour = wr.sent.date.hour(); @@ -172,14 +169,14 @@ impl WRs { let mut hist = HashMap::new(); for hour in 0..24 { - hist.insert(hour, 0 as u32); + hist.insert(hour, 0); } for wr in self.wrs.iter() { match wr.reply { Some(_) => { let hour = wr.sent.date.hour(); hist.entry(hour).and_modify(|e| *e += 1); - }, + } None => continue, }; } @@ -194,14 +191,15 @@ impl WRs { Some(ref cc) => { for addr in cc.iter() { if let Some(user) = &addr.user { - hist.entry(user.to_string()).and_modify(|e| *e += 1).or_insert(1 as u32); + hist.entry(user.to_string()) + .and_modify(|e| *e += 1) + .or_insert(1); } } - }, + } None => continue, }; } hist } - }