Send an email with Thinwrap (Gleam)

Send transactional email via thinwrap_notifications's send_email/2 in Gleam — one call shape for SendGrid, AWS SES, Resend and 7 more.

1. Install

gleam add thinwrap_notifications gleam_erlang envoy

2. Send the call

import envoy
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/atom
import gleam/io
import gleam/list

@external(erlang, "thinwrap_notifications", "send_email")
fn send_email(config: Dynamic, input: Dynamic) -> Result(Dynamic, Dynamic)

pub fn main() -> Nil {
  let assert Ok(api_key) = envoy.get("SENDGRID_API_KEY")
  let config =
    obj([
      #("provider", atom("sendgrid")),
      #("api_key", dynamic.string(api_key)),
      #("from", dynamic.string("noreply@example.com")),
    ])
  let input =
    obj([
      #("to", dynamic.string("user@example.com")),
      #("subject", dynamic.string("Hello from Thinwrap")),
      #("text", dynamic.string("Hello world")),
    ])

  case send_email(config, input) {
    // status is sent | queued | rejected | suppressed | unknown
    // provider_message_id is the upstream message id, or ""
    Ok(res) -> {
      let sent = {
        use status <- decode.field(key("status"), atom.decoder())
        use id <- decode.field(key("provider_message_id"), decode.string)
        decode.success(atom.to_string(status) <> " " <> id)
      }
      let assert Ok(line) = decode.run(res, sent)
      io.println(line)
    }
    Error(err) -> io.println(describe(err))
  }
}

// The library speaks atom-keyed Erlang maps; these helpers build and read them.
fn key(name: String) -> atom.Atom {
  atom.create(name)
}

fn atom(name: String) -> Dynamic {
  atom.to_dynamic(atom.create(name))
}

fn obj(fields: List(#(String, Dynamic))) -> Dynamic {
  dynamic.properties(list.map(fields, fn(f) { #(atom(f.0), f.1) }))
}

fn describe(err: Dynamic) -> String {
  let error = {
    use code <- decode.field(key("code"), atom.decoder())
    use message <- decode.field(key("message"), decode.string)
    decode.success(atom.to_string(code) <> ": " <> message)
  }
  let assert Ok(line) = decode.run(err, error)
  line
}

Same code works for all supported providers — just change the provider ID.