Overview
This guide covers advanced email workflows in artsend, focusing on the metrics documentation access system and integration patterns.
Metrics Access Request System
The metrics documentation site (metrics.artalytics.info) uses a gated access workflow with three email touchpoints:
- Confirmation to requester (immediate)
- Admin notification for review (immediate)
- Access granted notification (after approval)
System Architecture
User submits request form
↓
Database: Insert access_request record
↓
Email 1: send_metrics_confirm()
↓
Email 2: send_metrics_admin()
↓
Admin reviews request
↓
Database: Update status → "approved"
↓
Email 3: send_metrics_granted()
Step 1: User Submits Request
library(artsend)
library(data.table)
# Simulated form submission
form_data <- list(
email = "analyst@bank.com",
full_name = "Jane Doe",
company = "Major Bank Corp",
job_title = "Senior Credit Analyst",
role = "art_lender",
primary_interest = c("lending", "insurance"),
msg = "Evaluating our art lending portfolio. Need metrics guidance."
)
# Generate request ID
request_id <- uuid::UUIDgenerate()
# Create database record
access_request <- data.table(
req_id = request_id,
email = form_data$email,
full_name = form_data$full_name,
company = form_data$company,
job_title = form_data$job_title,
role = form_data$role,
primary_interest = jsonlite::toJSON(form_data$primary_interest),
msg = form_data$message,
status = "pending",
created_utc = lubridate::now("UTC")
)
# Insert to database
# cn <- artcore::dbc()
# DBI::dbAppendTable(cn, "metrics_access_requests", access_request)
# artcore::dbd(cn)Step 2: Send Confirmation to Requester
# Send immediate confirmation
confirmation_result <- send_metrics_confirm(
to = form_data$email,
full_name = form_data$full_name,
req_id = request_id
)
if (confirmation_result$success) {
rdstools::log_inf("Confirmation sent: {confirmation_result$message_id}")
# Update database
# data.table::setDT(access_request)[
# req_id == request_id,
# `:=`(
# confirmation_sent_utc = lubridate::now("UTC"),
# confirmation_id = confirmation_result$message_id
# )
# ]
} else {
rdstools::log_err("Confirmation failed: {confirmation_result$error}")
}Step 3: Notify Admin for Review
# Prepare request details for admin
request_details <- list(
email = form_data$email,
full_name = form_data$full_name,
company = form_data$company,
job_title = form_data$job_title,
role = form_data$role,
primary_interest = form_data$primary_interest,
msg = form_data$message,
req_id = request_id
)
# Send admin notification
admin_result <- send_metrics_admin(
req_details = request_details,
admin_email = "bobby@artalytics.app"
)
if (admin_result$success) {
rdstools::log_inf("Admin notified: {admin_result$message_id}")
} else {
rdstools::log_err("Admin notification failed: {admin_result$error}")
}Step 4: Admin Approves Request
# Admin reviews and approves (manual process or API call)
# Update database status
# data.table::setDT(access_request)[
# req_id == request_id,
# `:=`(
# status = "approved",
# approved_utc = lubridate::now("UTC"),
# approved_by = "bobby@artalytics.app"
# )
# ]
# Generate access credentials
access_code <- paste0("MTR-", format(Sys.Date(), "%Y"), "-",
stringr::str_pad(sample(1000:9999, 1), 4, pad = "0"))
access_instructions <- paste0(
"Your temporary access code: ", access_code, "\n\n",
"Visit: https://metrics.artalytics.info\n",
"Enter this code to unlock the full documentation.\n",
"Code expires: ", format(Sys.Date() + 30, "%Y-%m-%d")
)Step 5: Send Access Granted Notification
grant_result <- send_metrics_granted(
to = form_data$email,
full_name = form_data$full_name,
access_instructions = access_instructions
)
if (grant_result$success) {
rdstools::log_suc("Access granted email sent: {grant_result$message_id}")
# Update database
# data.table::setDT(access_request)[
# req_id == request_id,
# `:=`(
# access_code = access_code,
# access_granted_utc = lubridate::now("UTC"),
# access_granted_id = grant_result$message_id,
# code_expires_date = Sys.Date() + 30
# )
# ]
} else {
rdstools::log_err("Access grant email failed: {grant_result$error}")
}Complete Workflow Function
Wrap the entire workflow in a single function:
process_metrics_access_request <- function(form_data) {
# Validate inputs
required_fields <- c("email", "full_name")
missing <- required_fields[!required_fields %in% names(form_data)]
if (length(missing) > 0) {
return(list(
success = FALSE,
error = paste("Missing fields:", paste(missing, collapse = ", "))
))
}
# Generate request ID
request_id <- uuid::UUIDgenerate()
# Create database record
access_request <- data.table(
req_id = request_id,
email = form_data$email,
full_name = form_data$full_name,
company = if (is.null(form_data$company)) NA_character_ else form_data$company,
job_title = if (is.null(form_data$job_title)) NA_character_ else form_data$job_title,
role = if (is.null(form_data$role)) "other" else form_data$role,
primary_interest = jsonlite::toJSON(if (is.null(form_data$primary_interest)) list() else form_data$primary_interest),
msg = if (is.null(form_data$message)) NA_character_ else form_data$message,
status = "pending",
created_utc = lubridate::now("UTC")
)
# Insert to database
tryCatch({
cn <- artcore::dbc()
DBI::dbAppendTable(cn, "metrics_access_requests", access_request)
artcore::dbd(cn)
}, error = function(e) {
return(list(success = FALSE, error = paste("DB error:", e$message)))
})
# Send confirmation
conf_result <- send_metrics_confirm(
to = form_data$email,
full_name = form_data$full_name,
req_id = request_id
)
if (!conf_result$success) {
rdstools::log_wrn("Confirmation failed but request saved: {request_id}")
}
# Send admin notification
request_details <- c(list(req_id = request_id), form_data)
admin_result <- send_metrics_admin(request_details)
if (!admin_result$success) {
rdstools::log_wrn("Admin notification failed: {request_id}")
}
# Return success
list(
success = TRUE,
req_id = request_id,
confirmation_sent = conf_result$success,
admin_notified = admin_result$success
)
}Usage:
Shiny Integration
Use artsend in Shiny applications for interactive email workflows.
Contact Form Module
# Server logic for contact form
contactFormServer <- function(id, artist_email) {
moduleServer(id, function(input, output, session) {
# Email sending logic
observeEvent(input$submit_btn, {
# Validate inputs
if (input$visitor_name == "" || input$visitor_email == "" ||
input$message == "") {
showNotification("Please fill all fields", type = "error")
return()
}
# Show loading
shinybusy::show_spinner()
# Send email
result <- send_contact_email(
to = artist_email,
visitor_name = input$visitor_name,
visitor_email = input$visitor_email,
msg = input$message,
art_title = input$artwork_title,
art_url = input$artwork_url
)
# Hide loading
shinybusy::hide_spinner()
# Show result
if (result$success) {
showNotification("Message sent successfully!", type = "message")
# Reset form
updateTextInput(session, "visitor_name", value = "")
updateTextInput(session, "visitor_email", value = "")
updateTextAreaInput(session, "message", value = "")
} else {
showNotification(
paste("Failed to send:", result$error),
type = "error",
duration = NULL
)
}
})
})
}Waitlist Form
waitlistServer <- function(id) {
moduleServer(id, function(input, output, session) {
observeEvent(input$join_btn, {
# Validate email
if (!validate_email_fmt(input$email)) {
showNotification("Invalid email address", type = "error")
return()
}
# Create waitlist record
waitlist_id <- uuid::UUIDgenerate()
tryCatch({
# Insert to database
cn <- artcore::dbc()
DBI::dbExecute(cn, "
INSERT INTO artist_waitlist (id, email, artist_name, created_utc)
VALUES (?, ?, ?, ?)
", params = list(
waitlist_id,
input$email,
input$artist_name,
lubridate::now("UTC")
))
artcore::dbd(cn)
# Send confirmation
result <- send_waitlist_confirm(
to = input$email,
artist_name = input$artist_name,
waitlist_id = waitlist_id
)
if (result$success) {
showNotification(
"Welcome! Check your email for confirmation.",
type = "message",
duration = 8000
)
} else {
showNotification(
"Added to waitlist, but confirmation email failed.",
type = "warning"
)
}
}, error = function(e) {
showNotification(
paste("Error:", e$message),
type = "error",
duration = NULL
)
})
})
})
}Async Email Sending
For high-volume scenarios, send emails asynchronously using background jobs.
Using {callr}
library(callr)
# Background process
send_email_async <- function(email_function, ...) {
r_bg(
func = function(email_fn, args) {
do.call(email_fn, args)
},
args = list(
email_fn = email_function,
args = list(...)
),
supervise = TRUE
)
}
# Usage
bg_process <- send_email_async(
send_contact_email,
to = "artist@example.com",
visitor_name = "John Doe",
visitor_email = "john@example.com",
msg = "Great work!"
)
# Check status later
if (bg_process$is_alive()) {
message("Email still sending...")
} else {
result <- bg_process$get_result()
message("Email result: ", result$success)
}Batch Processing
Send multiple emails in sequence with error recovery:
send_batch_emails <- function(email_list) {
results <- data.table(
recipient = character(),
success = logical(),
message_id = character(),
error = character()
)
for (i in seq_along(email_list)) {
email <- email_list[[i]]
result <- send_contact_email(
to = email$to,
visitor_name = email$visitor_name,
visitor_email = email$visitor_email,
msg = email$message
)
results <- rbind(results, data.table(
recipient = email$to,
success = result$success,
message_id = if (is.null(result$message_id)) NA_character_ else result$message_id,
error = if (is.null(result$error)) NA_character_ else result$error
))
# Rate limiting: 100ms delay between emails
if (i < length(email_list)) {
Sys.sleep(0.1)
}
}
results
}
# Usage
emails <- list(
list(to = "artist1@example.com", visitor_name = "John", ...),
list(to = "artist2@example.com", visitor_name = "Jane", ...),
list(to = "artist3@example.com", visitor_name = "Bob", ...)
)
results <- send_batch_emails(emails)
# Summary
message(sprintf(
"Sent %d/%d emails successfully",
sum(results$success),
nrow(results)
))Custom Email Templates
While artsend provides pre-built templates, you can create custom emails for specific use cases.
Build Custom HTML
library(htmltools)
library(stringr)
build_custom_email <- function(recipient_name, content) {
html <- tags$html(
tags$head(
tags$meta(charset = "UTF-8"),
tags$meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
),
tags$body(
style = "font-family: Arial, sans-serif; margin: 0; padding: 20px;",
tags$div(
style = "max-width: 600px; margin: 0 auto;",
tags$h1(paste("Hello", recipient_name)),
tags$p(content),
tags$hr(),
tags$p(
style = "color: #666; font-size: 12px;",
"Artalytics | Digital Art Analytics"
)
)
)
)
as.character(html)
}
# Use with Resend API directly
config <- get_resend_config()
custom_html <- build_custom_email(
recipient_name = "Jane",
content = "Your custom message here."
)
response <- httr2::request(config$api_endpoint) |>
httr2::req_headers(
Authorization = paste("Bearer", config$api_key),
"Content-Type" = "application/json"
) |>
httr2::req_body_json(list(
from = config$from_email,
to = list("jane@example.com"),
subject = "Custom Email",
html = custom_html
)) |>
httr2::req_perform()Monitoring and Analytics
Track email delivery and engagement using the Resend dashboard and database logging.
Log All Emails
log_email_send <- function(email_type, recipient, result) {
log_entry <- data.table(
log_id = uuid::UUIDgenerate(),
email_type = email_type,
recipient = recipient,
resend_id = if (is.null(result$message_id)) NA_character_ else result$message_id,
success = result$success,
error_msg = if (is.null(result$error)) NA_character_ else result$error,
sent_utc = lubridate::now("UTC")
)
tryCatch({
cn <- artcore::dbc()
DBI::dbAppendTable(cn, "email_send_log", log_entry)
artcore::dbd(cn)
}, error = function(e) {
rdstools::log_err("Failed to log email: {e$message}")
})
}
# Usage
result <- send_contact_email(...)
log_email_send("contact_form", "artist@example.com", result)Query Email Stats
get_email_stats <- function(days = 7) {
cn <- artcore::dbc()
on.exit(artcore::dbd(cn))
query <- "
SELECT
email_type,
COUNT(*) AS total_sent,
SUM(CASE WHEN success THEN 1 ELSE 0 END) AS successful,
ROUND(100.0 * SUM(CASE WHEN success THEN 1 ELSE 0 END) / COUNT(*), 2) AS success_rate
FROM email_send_log
WHERE sent_utc >= NOW() - INTERVAL '? days'
GROUP BY email_type
ORDER BY total_sent DESC
"
DBI::dbGetQuery(cn, query, params = list(days)) |>
data.table::as.data.table()
}
# View stats
stats <- get_email_stats(days = 30)
print(stats)Best Practices
Environment variables: Always use environment variables for API keys. Never commit secrets.
Validation: Validate email addresses before sending to avoid wasting API calls.
Error handling: Always check result$success and handle failures gracefully.
Rate limiting: Resend free tier allows 100 emails/day. Implement delays for batch sends.
Logging: Log all email sends to database for tracking and debugging.
Testing: Use test API keys in development. Send to internal emails only.
Idempotency: Store resend_id to avoid duplicate sends.
Retry logic: Implement exponential backoff for transient failures.
Resources
- Resend API Documentation
- Resend Dashboard - View logs, delivery status
- Email Best Practices
- HTML Email Guide
Troubleshooting
Rate limit errors: Upgrade Resend plan or add delays between sends.
Emails in spam: Verify sender domain in Resend. Add SPF/DKIM records.
Invalid API key: Check ART_RESEND_KEY starts with re_. Verify in Resend dashboard.
Database connection errors: Ensure artcore::dbc() works. Check env vars.
