From 17137495ae5d638af0bf7b4cedfdb4a125f25417 Mon Sep 17 00:00:00 2001 From: almac2022 Date: Tue, 24 Feb 2026 07:39:08 -0800 Subject: [PATCH] Address expert review findings - Add send_log() and send_notify() for scheduled send outcomes (#2) - Replace hardcoded email default with getOption/env var pattern (#3) - Error on non-existent .md file paths in mc_compose (#4) - Extract shared strip_md_header() helper (#5) - Add mocked unit tests for MIME building, send_log, default_from (#6) - Update README with setup section and send log docs - Update vignette with send log notification docs Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Relates to NewGraphEnvironment/sred-2025-2026#4 Co-Authored-By: Claude Opus 4.6 --- .lintr | 5 +- R/mc_auth.R | 23 +++++- R/mc_compose.R | 10 +-- R/mc_md_render.R | 18 ++++- R/mc_send.R | 85 +++++++++++++++++++--- README.md | 23 ++++++ man/mc_auth.Rd | 8 ++- man/mc_send.Rd | 6 +- tests/testthat/test-mc_compose.R | 17 +++++ tests/testthat/test-mc_send.R | 119 +++++++++++++++++++++++++++++++ vignettes/tables-in-emails.Rmd | 3 + 11 files changed, 290 insertions(+), 27 deletions(-) diff --git a/.lintr b/.lintr index 5c9461b..33f6b49 100644 --- a/.lintr +++ b/.lintr @@ -1,4 +1 @@ -linters: linters_with_defaults( - quotes_linter = NULL, - line_length_linter = line_length_linter(120) - ) +linters: linters_with_defaults(quotes_linter = NULL, line_length_linter = line_length_linter(120), object_usage_linter = NULL) diff --git a/R/mc_auth.R b/R/mc_auth.R index 414e6eb..127d8cc 100644 --- a/R/mc_auth.R +++ b/R/mc_auth.R @@ -4,20 +4,39 @@ #' email address. Call once per session before [mc_send()]. #' #' @param email Email address to authenticate as. -#' Default `"al@newgraphenvironment.com"`. +#' Default uses `getOption("mc.from")`, then the `MC_FROM` environment +#' variable, then `"al@newgraphenvironment.com"`. #' #' @return Invisible `NULL`. Called for side effect of authenticating. #' #' @examples #' \dontrun{ #' mc_auth() +#' +#' # Set globally in .Rprofile to avoid passing email every time: +#' options(mc.from = "you@example.com") #' } #' #' @importFrom chk chk_string #' @importFrom gmailr gm_auth #' @export -mc_auth <- function(email = "al@newgraphenvironment.com") { +mc_auth <- function(email = default_from()) { chk::chk_string(email) gmailr::gm_auth(email = email) invisible(NULL) } + + +#' Get the default sender address +#' +#' Checks `getOption("mc.from")`, then `MC_FROM` env var, then falls back +#' to `"al@newgraphenvironment.com"`. +#' @return Character string. +#' @noRd +default_from <- function() { + from <- getOption("mc.from") + if (!is.null(from)) return(from) + env <- Sys.getenv("MC_FROM", unset = "") + if (nzchar(env)) return(env) + "al@newgraphenvironment.com" +} diff --git a/R/mc_compose.R b/R/mc_compose.R index a0f1089..e3bab10 100644 --- a/R/mc_compose.R +++ b/R/mc_compose.R @@ -96,12 +96,12 @@ resolve_part <- function(part) { } # Markdown file — render it - if (grepl("\\.md$", part, ignore.case = TRUE) && file.exists(part)) { - raw <- paste(readLines(part, warn = FALSE), collapse = "\n") - # Strip header above --- if present - if (grepl("---", raw, fixed = TRUE)) { - raw <- sub("^[\\s\\S]*?---\\s*\\n", "", raw, perl = TRUE) + if (grepl("\\.md$", part, ignore.case = TRUE)) { + if (!file.exists(part)) { + stop("File not found: ", part, call. = FALSE) } + raw <- paste(readLines(part, warn = FALSE), collapse = "\n") + raw <- strip_md_header(raw) return(commonmark::markdown_html(raw, extensions = TRUE)) } diff --git a/R/mc_md_render.R b/R/mc_md_render.R index b190c57..e06bdbf 100644 --- a/R/mc_md_render.R +++ b/R/mc_md_render.R @@ -54,7 +54,7 @@ mc_md_render <- function(path, sig = TRUE, sig_path = NULL) { raw <- paste(readLines(path, warn = FALSE), collapse = "\n") # Strip header: everything up to and including the --- separator - body_md <- sub("^[\\s\\S]*?---\\s*\\n", "", raw, perl = TRUE) + body_md <- strip_md_header(raw) # Convert markdown to HTML body_html <- commonmark::markdown_html(body_md, extensions = TRUE) @@ -72,6 +72,22 @@ mc_md_render <- function(path, sig = TRUE, sig_path = NULL) { } +#' Strip the header above the first `---` separator in markdown +#' +#' Used by both [mc_md_render()] and `resolve_part()` in [mc_compose()]. +#' If no `---` is found, the full text is returned unchanged. +#' @param md Character string of raw markdown. +#' @return Markdown with header stripped. +#' @noRd +strip_md_header <- function(md) { + if (grepl("---", md, fixed = TRUE)) { + sub("^[\\s\\S]*?---\\s*\\n", "", md, perl = TRUE) + } else { + md + } +} + + #' Add inline styles to HTML tables for Gmail #' #' Gmail strips `