From c10e1e2fb9c5c65aedef128c7801243545ecac93 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:49:49 -0500 Subject: [PATCH] Emit json fields without rsc_ prefix if the connect environment version is after 2025.11 future support could allow other input flags to trigger this behavior --- src/resources/filters/modules/constants.lua | 7 +- src/resources/filters/quarto-post/email.lua | 131 ++++++++++++++++---- tests/smoke/render/render-email.test.ts | 19 +++ 3 files changed, 131 insertions(+), 26 deletions(-) diff --git a/src/resources/filters/modules/constants.lua b/src/resources/filters/modules/constants.lua index 7153e486581..6127b75c731 100644 --- a/src/resources/filters/modules/constants.lua +++ b/src/resources/filters/modules/constants.lua @@ -161,6 +161,9 @@ local kBackgroundColorCaution = "ffe5d0" local kIncremental = "incremental" local kNonIncremental = "nonincremental" +-- Connect version requirements +local kConnectEmailMetadataChangeVersion = "2025.11" + return { kCitation = kCitation, kContainerId = kContainerId, @@ -254,5 +257,7 @@ return { kBackgroundColorCaution = kBackgroundColorCaution, kIncremental = kIncremental, - kNonIncremental = kNonIncremental + kNonIncremental = kNonIncremental, + + kConnectEmailMetadataChangeVersion = kConnectEmailMetadataChangeVersion } diff --git a/src/resources/filters/quarto-post/email.lua b/src/resources/filters/quarto-post/email.lua index 836e037d47d..1f9b766d468 100644 --- a/src/resources/filters/quarto-post/email.lua +++ b/src/resources/filters/quarto-post/email.lua @@ -17,6 +17,8 @@ Extension for generating email components needed for Posit Connect (this can be disabled by setting `email-preview: false` in the YAML header) --]] +local constants = require("modules/constants") + -- Get the file extension of any file residing on disk function get_file_extension(file_path) local pattern = "%.([^%.]+)$" @@ -63,6 +65,71 @@ function str_truthy_falsy(str) return false end +-- Parse Connect version from SPARK_CONNECT_USER_AGENT +-- Format: posit-connect/2024.09.0 +--- posit-connect/2024.09.0-dev+26-g51b853f70e +--- posit-connect/2024.09.0-dev+26-dirty-g51b853f70e +-- Returns: "2024.09.0" or nil +function get_connect_version() + local user_agent = os.getenv("SPARK_CONNECT_USER_AGENT") + if not user_agent then + return nil + end + + -- Extract the version after "posit-connect/" + local version_with_suffix = string.match(user_agent, "posit%-connect/([%d%.%-+a-z]+)") + if not version_with_suffix then + return nil + end + + -- Strip everything after the first "-" (e.g., "-dev+88-gda902918eb") + local idx = string.find(version_with_suffix, "-") + if idx then + return string.sub(version_with_suffix, 1, idx - 1) + end + + return version_with_suffix +end + +-- Parse a version string into components +-- Versions are in format "X.Y.Z", with all integral components (e.g., "2025.11.0") +-- Returns: {major=2025, minor=11, patch=0} or nil +function parse_version_components(version_string) + if not version_string then + return nil + end + + -- Parse version (e.g., "2025.11.0" or "2025.11") + local major, minor, patch = string.match(version_string, "^(%d+)%.(%d+)%.?(%d*)$") + if not major then + return nil + end + + return { + major = tonumber(major), + minor = tonumber(minor), + patch = patch ~= "" and tonumber(patch) or 0 + } +end + +-- Check if Connect version is >= target version +-- Versions are in format "YYYY.MM.patch" (e.g., "2025.11.0") +function is_connect_version_at_least(target_version) + local current_version = get_connect_version() + local current = parse_version_components(current_version) + local target = parse_version_components(target_version) + + if not current or not target then + return false + end + + -- Convert to numeric YYYYMMPP format and compare + local current_num = current.major * 10000 + current.minor * 100 + current.patch + local target_num = target.major * 10000 + target.minor * 100 + target.patch + + return current_num >= target_num +end + local html_email_template_1 = [[ @@ -190,6 +257,7 @@ local email_images = {} local image_tbl = {} local suppress_scheduled_email = false local found_email_div = false +local use_new_email_format = false function process_meta(meta) if not found_email_div then @@ -201,6 +269,12 @@ function process_meta(meta) local meta_email_attachments = meta["email-attachments"] meta_email_preview = meta["email-preview"] + -- Auto-detect Connect version and use appropriate email format + -- Connect 2025.11+ uses new format (no rsc_ prefix) + if is_connect_version_at_least(constants.kConnectEmailMetadataChangeVersion) then + use_new_email_format = true + end + if meta_email_attachments ~= nil then for _, v in pairs(meta_email_attachments) do if (file_exists(pandoc.utils.stringify(v))) then @@ -306,6 +380,25 @@ function extract_email_div_str(doc) return pandoc.write(pandoc.Pandoc( {doc} ), "html") end +-- Function to build email metadata with appropriate field names +function build_email_metadata(subject, attachments, html_email_body, email_text, email_images, suppress_scheduled_email) + local prefix = use_new_email_format and "" or "rsc_" + + local metadata = {} + metadata[prefix .. "email_subject"] = subject + metadata[prefix .. "email_attachments"] = attachments + metadata[prefix .. "email_body_html"] = html_email_body + metadata[prefix .. "email_body_text"] = email_text + metadata[prefix .. "email_suppress_report_attachment"] = true + metadata[prefix .. "email_suppress_scheduled"] = suppress_scheduled_email + + if not is_empty_table(email_images) then + metadata[prefix .. "email_images"] = email_images + end + + return metadata +end + function process_document(doc) if not found_email_div then @@ -374,30 +467,18 @@ function process_document(doc) -- Encode all of the strings and tables of strings into the JSON file -- (`.output_metadata.json`) that's needed for Connect's email feature - - if (is_empty_table(email_images)) then - - metadata_str = quarto.json.encode({ - rsc_email_subject = subject, - rsc_email_attachments = attachments, - rsc_email_body_html = html_email_body, - rsc_email_body_text = email_text, - rsc_email_suppress_report_attachment = true, - rsc_email_suppress_scheduled = suppress_scheduled_email - }) - - else - - metadata_str = quarto.json.encode({ - rsc_email_subject = subject, - rsc_email_attachments = attachments, - rsc_email_body_html = html_email_body, - rsc_email_body_text = email_text, - rsc_email_images = email_images, - rsc_email_suppress_report_attachment = true, - rsc_email_suppress_scheduled = suppress_scheduled_email - }) - end + -- Uses the appropriate field names based on Connect version/configuration + + local metadata = build_email_metadata( + subject, + attachments, + html_email_body, + email_text, + email_images, + suppress_scheduled_email + ) + + metadata_str = quarto.json.encode(metadata) -- Determine the location of the Quarto project directory; if not defined -- by the user then set to the location of the input file @@ -450,4 +531,4 @@ function render_email() Div = process_div, } } -end \ No newline at end of file +end diff --git a/tests/smoke/render/render-email.test.ts b/tests/smoke/render/render-email.test.ts index bcb1153431f..b28623e9e1c 100644 --- a/tests/smoke/render/render-email.test.ts +++ b/tests/smoke/render/render-email.test.ts @@ -38,6 +38,22 @@ testRender(docs("email/email-attach.qmd"), "email", false, [fileExists(previewFi // Test an email render that has no subject line, this verifies that `rsc_email_subject` key is present and the value is an empty string testRender(docs("email/email-no-subject.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": ""})], cleanupCtx); +// Test a basic email render, verifies that the outputs are without the rsc_ prefix +testRender(docs("email/email.qmd"), "email", false, [validJsonWithFields(jsonFile, {"email_subject": "The subject line."})], { + ...cleanupCtx, + env: { + "SPARK_CONNECT_USER_AGENT": "posit-connect/2025.11.0" + } +}); + +// Test a basic email render, verifies that the outputs are with the rsc_ prefix given a 2024 version +testRender(docs("email/email.qmd"), "email", false, [validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line."})], { + ...cleanupCtx, + env: { + "SPARK_CONNECT_USER_AGENT": "posit-connect/2024.12.0-dev+26-g51b853f70e" + } +}); + // Render in a project with an output directory set in _quarto.yml and confirm that everything ends up in the output directory testProjectRender(docs("email/project/email-attach.qmd"), "email", (outputDir: string) => { const verify: Verify[]= []; @@ -50,3 +66,6 @@ testProjectRender(docs("email/project/email-attach.qmd"), "email", (outputDir: s verify.push(validJsonWithFields(json, {"rsc_email_subject": "The subject line.", "rsc_email_attachments": ["raw_data.csv"]})); return verify; }); + + +