Skip to contents

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:

  1. Confirmation to requester (immediate)
  2. Admin notification for review (immediate)
  3. 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:

result <- process_metrics_access_request(
  form_data = list(
    email = "analyst@bank.com",
    full_name = "Jane Doe",
    company = "Major Bank",
    role = "art_lender"
  )
)

if (result$success) {
  message("Request processed: ", result$request_id)
} else {
  warning("Request failed: ", result$error)
}

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

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.