app
Resend Studio
Public Made by Adomby adom
A local editor for Resend email templates: paste your API key, then edit, preview, lint, test-send, and publish templates without Resend's dashboard. Also imports SendGrid dynamic templates and converts their Handlebars to Resend's variable syntax.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
//! Read-only SendGrid access, used only to import templates during the move
//! to Resend. Nothing here writes to SendGrid.
use anyhow::{Context, Result, anyhow};
use serde_json::Value;
const API_BASE: &str = "https://api.sendgrid.com/v3";
pub struct SendGridClient {
api_key: String,
}
/// The active version of one SendGrid dynamic template.
#[derive(Debug, serde::Serialize, Clone)]
pub struct ActiveTemplate {
pub id: String,
pub name: String,
pub subject: String,
pub html: String,
}
impl SendGridClient {
pub fn new(api_key: String) -> Self {
Self { api_key }
}
fn get(&self, path: &str) -> Result<Value> {
ureq::get(&format!("{API_BASE}{path}"))
.set("Authorization", &format!("Bearer {}", self.api_key))
.timeout(std::time::Duration::from_secs(20))
.call()
.map_err(|e| match e {
ureq::Error::Status(code, resp) => {
anyhow!("SendGrid API returned HTTP {code}: {}", resp.into_string().unwrap_or_default())
}
ureq::Error::Transport(t) => anyhow!("SendGrid API request failed: {t}"),
})?
.into_json()
.context("parsing SendGrid response")
}
/// Every dynamic template with its active version's subject and HTML.
pub fn list_active(&self) -> Result<Vec<ActiveTemplate>> {
let list = self.get("/templates?generations=dynamic&page_size=200")?;
let ids: Vec<String> = list
.get("result")
.and_then(|r| r.as_array())
.map(|a| a.iter().filter_map(|t| t.get("id")?.as_str().map(String::from)).collect())
.unwrap_or_default();
let mut out = Vec::new();
for id in ids {
if let Some(t) = self.get_active(&id)? {
out.push(t);
}
}
Ok(out)
}
pub fn get_active(&self, id: &str) -> Result<Option<ActiveTemplate>> {
let t = self.get(&format!("/templates/{id}"))?;
let name = t.get("name").and_then(|v| v.as_str()).unwrap_or(id).to_string();
let versions = t.get("versions").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let Some(v) = versions.iter().find(|v| v.get("active").and_then(|a| a.as_i64()) == Some(1)) else {
return Ok(None);
};
Ok(Some(ActiveTemplate {
id: id.to_string(),
name,
subject: v.get("subject").and_then(|s| s.as_str()).unwrap_or("").to_string(),
html: v.get("html_content").and_then(|s| s.as_str()).unwrap_or("").to_string(),
}))
}
}
/// Sample values from a SendGrid Studio database, keyed by SendGrid template
/// id then variable name, so imported drafts keep the preview data that was
/// already written for them.
pub fn studio_samples(db_path: &str) -> std::collections::HashMap<String, serde_json::Map<String, Value>> {
let mut out = std::collections::HashMap::new();
let Ok(conn) = rusqlite::Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY) else {
return out;
};
let Ok(mut stmt) = conn.prepare("SELECT template_id, variables_json FROM manifests") else {
return out;
};
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)));
if let Ok(rows) = rows {
for (id, vars) in rows.flatten() {
let mut map = serde_json::Map::new();
if let Ok(Value::Array(arr)) = serde_json::from_str::<Value>(&vars) {
for v in arr {
if let (Some(name), Some(sample)) = (v.get("name").and_then(|n| n.as_str()), v.get("sample")) {
map.insert(name.to_string(), sample.clone());
}
}
}
out.insert(id, map);
}
}
out
}