diff --git a/Cargo.lock b/Cargo.lock index 92f3eff..a639e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,23 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chrono_track" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "clap", + "csv", + "lettre", + "log", + "reqwest", + "serde", + "serde_json", + "simplelog", + "toml", +] + [[package]] name = "clap" version = "4.4.2" @@ -1555,23 +1572,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "time_tracker" -version = "0.1.0" -dependencies = [ - "chrono", - "chrono-tz", - "clap", - "csv", - "lettre", - "log", - "reqwest", - "serde", - "serde_json", - "simplelog", - "toml", -] - [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index a19461f..273f454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] members = [ "status_cloud", - "time_tracker" + "chrono_track" ] diff --git a/time_tracker/Cargo.toml b/chrono_track/Cargo.toml similarity index 96% rename from time_tracker/Cargo.toml rename to chrono_track/Cargo.toml index cac90bc..3b44e57 100644 --- a/time_tracker/Cargo.toml +++ b/chrono_track/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "time_tracker" +name = "chrono_track" version = "0.1.0" edition = "2021" diff --git a/time_tracker/src/main.rs b/chrono_track/src/main.rs similarity index 78% rename from time_tracker/src/main.rs rename to chrono_track/src/main.rs index 7ca3091..5bb244b 100644 --- a/time_tracker/src/main.rs +++ b/chrono_track/src/main.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Datelike, Utc}; use chrono_tz::US::Central; use clap::Parser; use lettre::{ - message::{Message, header::ContentType, Attachment, MultiPart, SinglePart}, + message::{header::ContentType, Attachment, Message, MultiPart, SinglePart}, SendmailTransport, Transport, }; use log::{debug, error}; @@ -196,7 +196,7 @@ fn main() -> Result<(), Box> { if do_email_summary { debug!("Send email selected"); reset_email_checkbox(&cfg); - send_email_summary(&cfg, body_content); + send_email_summary(&cfg, body_content, &args); } Ok(()) @@ -236,6 +236,9 @@ struct CliArgs { #[arg(short, long)] config_file: PathBuf, + #[arg(short, long)] + from_addr: String, + #[arg(short, long)] debug: bool, } @@ -299,14 +302,14 @@ struct SummaryRow { // All calls after depend on the success of the one before, and cannot run // without. So it would be too much work to make an error type to handle an error // that would cause a crash anyways -fn send_email_summary(config: &Config, body_content: Vec) { +fn send_email_summary(config: &Config, body_content: Vec, cliargs: &CliArgs) { let mut wtr = csv::Writer::from_writer(vec![]); for line in body_content { if line.starts_with("### ") { let mut split: Vec<&str> = line.split('|').collect(); if split.len() != 3 { - error!("There was an issue with this line: {line}"); + error!("There was an issue with this line. 3 splits not found: {line}"); continue; } let date: String = match split.pop() { @@ -355,33 +358,97 @@ fn send_email_summary(config: &Config, body_content: Vec) { .to_string() .replace("### ", ""); - wtr.serialize(SummaryRow { date: date.trim().to_string(), total_time: time, task_name }).unwrap(); + match wtr.serialize(SummaryRow { + date: date.trim().to_string(), + total_time: time, + task_name, + }) { + Ok(_) => continue, + Err(e) => { + error!("There was an error serializing the csv, aborting: {e}"); + return; + } + }; } } - let data = String::from_utf8(wtr.into_inner().unwrap()).unwrap(); + let data = match String::from_utf8(wtr.into_inner().unwrap()) { + Ok(val) => val, + Err(e) => { + error!("There was an error converting the csv writer to a string, aborting: {e}"); + return; + } + }; debug!("{:#?}", data); - let attachment = Attachment::new("TimeSummary.csv".to_string()).body(data, ContentType::parse("text/csv").unwrap()); + let attachment = Attachment::new("TimeSummary.csv".to_string()) + // The unwrap is on a constant value + .body(data, ContentType::parse("text/csv").unwrap()); + + const HTML: &str = r#" + + + + + + ChronoTrack Export + + +
+
+ +
+
+
+

Prepared with care, and sent through the horrors of the interwebs, ChronoTrack presents! (see attached)

+
+ + +"#; let email = Message::builder() - .from("NoReplay ".parse().unwrap()) - .to("Nicholas Young ".parse().unwrap()) - .subject("Testing email") - .header(ContentType::TEXT_PLAIN) + .from(match cliargs.from_addr.parse() { + Ok(val) => val, + Err(e) => { + error!("The provided from_email address was unparsable:\n{e}"); + std::process::exit(1); + } + }) + .to(match config.email_addr.parse() { + Ok(val) => val, + Err(e) => { + error!( + "The provided destination email in the configuration file was unparsable:\n{e}" + ); + std::process::exit(1); + } + }) + .subject("ChronoTrack Export") .multipart( MultiPart::mixed() - .singlepart(SinglePart::plain("Hello World".to_string())) - .singlepart(attachment) - ).unwrap(); - + .multipart( + MultiPart::alternative() + .singlepart( + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(String::from("This is an automated email from ChronoTrack")), + ) + .singlepart( + SinglePart::builder() + .header(ContentType::TEXT_HTML) + .body(String::from(HTML)), + ), + ) + .singlepart(attachment), + ) + .unwrap(); let mailer = SendmailTransport::new(); - + match mailer.send(&email) { Ok(val) => debug!("email sent: {:?}", val), - Err(e) => error!("Couldn't send email {}", e) + Err(e) => error!("Couldn't send email {}", e), }; - } diff --git a/flake.nix b/flake.nix index defc7f4..3c87fd2 100644 --- a/flake.nix +++ b/flake.nix @@ -37,7 +37,8 @@ rust-project TODO: write shell script for automatically updating `cargoHash` rustSettings = with pkgs; { src = ./.; nativeBuildInputs = [ pkg-config ]; - buildInputs = [ openssl hddtemp msmtp ]; + # move makeWrapper to nativeBuildInputs + buildInputs = [ openssl hddtemp msmtp makeWrapper ]; cargoHash = nixpkgs.lib.fakeHash; }; meta = with nixpkgs.lib; { @@ -61,26 +62,27 @@ rust-project TODO: write shell script for automatically updating `cargoHash` pname = "status_cloud"; version = "0.1.0"; buildAndTestSubdir = "status_cloud"; - cargoHash = "sha256-XIonh2SQ8EichcZvHErSydt0qtKGodXE0yKz4BsntA8="; - preBuild = '' - sed -i 's/Command::new("hddtemp")/Command::new("${nixpkgs.lib.escape [ "/" ] "${pkgs.hddtemp}"}")/g' status_cloud/src/main.rs + cargoHash = "sha256-mXcD/92WJLgN2efX/8EiV3rOqUiNOKGC4puS8DvGFOc="; + postFixup = '' + wrapProgram $out/bin/status_cloud \ + --prefix PATH : "${nixpkgs.lib.makeBinPath [ pkgs.hddtemp ]}" ''; meta = meta // { description = "Server status saved to a nextcloud note service."; }; }); - time_tracker = pkgs.rustPlatform.buildRustPackage (rustSettings // { - pname = "time_tracker"; + chrono_track = pkgs.rustPlatform.buildRustPackage (rustSettings // { + pname = "chrono_track"; version = "0.1.0"; - buildAndTestSubdir = "time_tracker"; - cargoHash = "sha256-5Wy2+ef2BriKmvNhllDVh/clAZGV7myDc281ygj05vI="; + buildAndTestSubdir = "chrono_track"; + cargoHash = "sha256-51j5eG2ZBJg2TjbvYSEvvmOM4hhPyNohN+LUmvck88k="; meta = meta // { description = "Using nextcloud notes to track time usage."; }; postFixup = '' - wrapProgram $out/bin/time_tracker \ - --prefix PATH : "${lib.makeBinPath [ msmtp ]}" + wrapProgram $out/bin/chrono_track \ + --prefix PATH : "${nixpkgs.lib.makeBinPath [ pkgs.msmtp ]}" ''; }); }; @@ -137,14 +139,14 @@ rust-project TODO: write shell script for automatically updating `cargoHash` # Time Tracker - options.services.time_tracker = { - enable = lib.mkEnableOption (lib.mdDoc "time tracker service"); + options.services.chrono_track = { + enable = lib.mkEnableOption (lib.mdDoc "chrono track, time tracking service"); package = lib.mkOption { type = lib.types.package; - default = self.packages.${system}.time_tracker; - defaultText = "pkgs.time_tracker"; + default = self.packages.${system}.chrono_track; + defaultText = "pkgs.chrono_track"; description = lib.mdDoc '' - The time_tracker package that should be used + The chrono_track package that should be used ''; }; config_path = lib.mkOption { @@ -153,6 +155,12 @@ rust-project TODO: write shell script for automatically updating `cargoHash` The file path to the toml that contains user information secrets ''; }; + from_address = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + The from address for the emails. E.g. noreply@example.com + ''; + }; frequency = lib.mkOption { type = lib.types.int; description = lib.mdDoc '' @@ -161,25 +169,25 @@ rust-project TODO: write shell script for automatically updating `cargoHash` }; }; - config.systemd.services.time_tracker = let - cfg = config.services.time_tracker; - pkg = self.packages.${system}.time_tracker; + config.systemd.services.chrono_track = let + cfg = config.services.chrono_track; + pkg = self.packages.${system}.chrono_track; in lib.mkIf cfg.enable { - description = "Nextcloud Time Tracker"; + description = "Chrono Track"; serviceConfig = { Type = "oneshot"; ExecStart = '' - ${cfg.package}/bin/time_tracker --config-file ${builtins.toString cfg.config_path} + ${cfg.package}/bin/chrono_track --config-file ${builtins.toString cfg.config_path} --from-addr ${cfg.from_address} ''; }; }; - config.systemd.timers.time_tracker = let - cfg = config.services.time_tracker; - pkg = self.packages.${system}.time_tracker; + config.systemd.timers.chrono_track = let + cfg = config.services.chrono_track; + pkg = self.packages.${system}.chrono_track; in lib.mkIf cfg.enable { wantedBy = [ "timers.target" ]; - partOf = [ "time_tracker.service" ]; + partOf = [ "chrono_track.service" ]; timerConfig.OnCalendar = [ "*:0/${builtins.toString cfg.frequency}" ]; };