Courrier

API-powered email delivery and newsletter subscription management for Ruby apps

A cute cartoon mascot wearing a blue postal uniform with red scarf and cap, carrying a leather messenger bag, representing an API-powered email delivery gem for Ruby apps

# Quick example
class OrderEmail < Courrier::Email
  def subject = "Here is your order!"

  def text = "Thanks for ordering"

  def html = "<p>Thanks for ordering</p>"
end

OrderEmail.deliver to: "recipient@railsdesigner.com"

# Manage newsletter subscribers
Courrier::Subscriber.create "subscriber@example.com"
Courrier::Subscriber.destroy "subscriber@example.com"

Rails Designer logo

Sponsored By Rails Designer

Installation

Add the gem:

bundle add courrier

[!tip] For Rails apps, use rails_courrier instead. It includes generators, ActiveJob support (deliver_later), inbox previews and more.

Configure Courrier in your app:

Courrier.configure do |config|
  config.email = {
    provider: "postmark",
    api_key: "your-api-key"
  }

  config.from = "devs@example.com"
end

Usage

class OrderEmail < Courrier::Email
  def subject = "Here is your order!"

  def text
    <<~TEXT
      text body here
    TEXT
  end

  def html
    <<~HTML
      html body here
    HTML
  end
end

# OrderEmail.deliver to: "recipient@railsdesigner.com"

Configuration

Courrier uses a configuration system with three levels (from lowest to highest priority):

  1. Global configuration ```ruby Courrier.configure do |config| config.email = { provider: "postmark", api_key: "xyz" }

config.from = "devs@railsdesigner.com" config.default_url_options = { host: "railsdesigner.com" }

# Provider-specific configuration config.providers.cloudflare.account_id = "your-account-id" config.providers.loops.transactional_id = "default-template" config.providers.mailgun.domain = "notifications.railsdesigner.com"

config.providers.ses.region = "us-east-1" config.providers.ses.access_key_id = "your-access-key-id" config.providers.ses.secret_access_key = "your-secret-access-key" end


2. **Email class defaults**
```ruby
class OrderEmail < Courrier::Email
  configure from: "orders@railsdesigner.com",
            cc: "records@railsdesigner.com",
            provider: "mailgun"
end
  1. Instance options ruby OrderEmail.deliver to: "recipient@railsdesigner.com",\ from: "shop@railsdesigner.com",\ provider: "sendgrid",\ api_key: "sk_a1b1c3"

Provider and API key settings can be overridden using environment variables (COURRIER_PROVIDER and COURRIER_API_KEY) for both global configuration and email class defaults.

Custom headers

Email classes can define custom HTTP headers that are sent with every email:

class OrderEmail < Courrier::Email
  headers list_unsubscribe_post: "List-Unsubscribe=One-Click"

  def subject = "Rails Icons now supports SVG sprites!"

  def text = # …

  def html = # …
end

Useful for adding provider-specific headers like List-Unsubscribe for Postmark, X-Mailer identifiers or custom metadata headers required.

Custom attributes

Besides the standard email attributes (from, to, reply_to, etc.), you can pass any additional attributes that will be available in your email templates:

OrderEmail.deliver to: "recipient@railsdesigner.com",\
                   download_url: "https://example.com/download?token=abc123"

These custom attributes are accessible directly in your email class:

def text
  <<~TEXT
    #{download_url}
  TEXT
end

Result object

When sending an email through Courrier, a Result object is returned that provides information about the delivery attempt. This object offers a simple interface to check the status and access response data.

Available methods

Method Return Type Description
success? Boolean Returns true if the API request was successful
response Net::HTTP::Response The raw HTTP response from the email provider
data Hash Parsed JSON response body from the provider
error Exception Contains any error that occurred during delivery

Example

delivery = OrderEmail.deliver to: "recipient@example.com"

if delivery.success?
  puts "Email sent successfully!"
  puts "Provider response: #{delivery.data}"
else
  puts "Failed to send email: #{delivery.error}"
end

Providers

Courrier supports these transactional email providers:

More Features

Additional functionality to help with development and testing:

Layout support

Wrap your email content using layouts:

class OrderEmail < Courrier::Email
 layout text: "%{content}\n\nThanks for your order!",
        html: "<div>\n%{content}\n</div>"
end

Using a method:

class OrderEmail < Courrier::Email
  layout html: :html_layout

  def html_layout
    <<~HTML
      <div style='font-family: ui-sans-serif, system-ui;'>
        %{content}
      </div>
    HTML
  end
end

Using a separate class:

class OrderEmail < Courrier::Email
  layout html: OrderLayout
end

class OrderLayout
  self.call
    <<~HTML
      <div style='font-family: ui-sans-serif, system-ui;'>
        %{content}
      </div>
    HTML
  end
end

Template files

Instead of defining text and html methods, you can create ERB template files:

class OrderEmail < Courrier::Email
  def subject = "Your order is ready!"

  # text and html content will be loaded from template files
end

Create template files alongside your email class (default path is courrier/emails):

  • courrier/emails/order_email.text.erb
  • courrier/emails/order_email.html.erb

Templates have access to all context options and instance variables:

<!-- courrier/emails/order_email.html.erb -->
<h1>Hello <%= name %>!</h1>
<p>Your order #<%= order_id %> is ready for pickup.</p>

Method definitions take precedence over template files when both exist. You can mix approaches. For example, define text in a method and use a template for the html:

class OrderEmail < Courrier::Email
  def subject = "Your order is ready!"

  def text = "Hello #{name}! Your order ##{order_id} is ready."

  # html will be loaded from courrier/emails/order_email.html.erb
end

Markdown support

Courrier supports rendering markdown content to HTML when a markdown gem is available. Simply bundle any supported markdown gem (redcarpet, kramdown or commonmarker) and it will be used.

Markdown methods

Define a markdown method in your email class:

class OrderEmail < Courrier::Email
  def subject = "Your order is ready!"

  def markdown
    <<~MARKDOWN
      # Hello #{name}!

      Your order **##{order_id}** is ready for pickup.

      ## Order Details
      - Item: #{item_name}
      - Price: #{price}
    MARKDOWN
  end
end

Markdown templates

Create markdown template files alongside your email class (default path is courrier/emails):

  • courrier/emails/order_email.md.erb
  • courrier/emails/order_email.markdown.erb
<!-- courrier/emails/order_email.md.erb -->
# Hello <%= name %>!

Your order **#<%= order_id %>** is ready for pickup.

## Order Details
- Item: <%= item_name %>
- Price: <%= price %>

Method definitions take precedence over template files. You can mix approaches. For example, define text in a method and use a markdown template for HTML content.

Auto-generate text from HTML

Automatically generate plain text versions from your HTML emails:

config.auto_generate_text = true  # defaults to false

Email address helper

Compose email addresses with display names:

class Signup
  include Courrier::Email::Address

  def send_welcome_email(user)
    recipient = email_with_name(user.email_address, user.name)

    WelcomeEmail.deliver to: recipient
  end
end

Logger provider

Use Ruby's built-in Logger for development and testing:

config.provider = "logger"  # outputs emails to STDOUT
config.logger = custom_logger  # optional: defaults to ::Logger.new($stdout)

Custom providers

Create your own provider by inheriting from Courrier::Email::Providers::Base:

class CustomProvider < Courrier::Email::Providers::Base
  ENDPOINT_URL = ""

  def body = ""

  def headers = ""
end

Then configure it:

config.provider = "CustomProvider"

Check the existing providers for implementation examples.

Testing

Courrier provides Test and TestHelper for testing email delivery, similar to Action Mailer's testing API.

Access all delivered emails:

# Clear deliveries between tests
Courrier::Test.clear!

# Access all deliveries
Courrier::Test.deliveries

Each delivery record contains:

  • email_class; the email class name
  • to, from, reply_to, cc, bcc; email addresses
  • subject; email subject
  • body - Hash with :text and :html keys
  • headers; custom headers
  • provider; provider used
  • result; result object with success? method
  • timestamp; delivery time

Include the helper in your test class for assertions:

class OrderTest < Minitest::Test
  include Courrier::TestHelper

  def setup
    Courrier::Test.clear!
  end

  def test_sends_confirmation_email
    order = Order.create! product: "Widget", customer_email: "customer@example.com"

    OrderEmail.deliver to: order.customer_email, order: order

    assert_emails_delivered 1
    assert_email_delivered to: "customer@example.com"
    assert_email_delivered OrderEmail, subject: "Order"
  end

  def test_no_emails_when_order_cancelled
    order = Order.create! product: "Widget", status: :cancelled

    assert_no_emails_delivered
  end
end

Available assertions:

  • assert_emails_delivered(count); assert the number of emails delivered
  • assert_no_emails_delivered; assert no emails were delivered
  • assert_email_delivered(email_class, to:, from:, subject:, provider:); assert an email matching criteria was delivered

Delivery callbacks

Hook into the email delivery lifecycle with before_deliver and after_deliver callbacks:

class OrderEmail < Courrier::Email
  before_deliver do |email|
    puts "Sending to #{email.options.to}"  # access email options, abort delivery by returning false
  end

  after_deliver do |email, result|
    puts "Delivered: #{result.success?}"  # access email and delivery result
  end
end

Callbacks are isolated per class (subclasses don't inherit parent callbacks).

Newsletter subscriptions

Manage subscribers across popular email marketing platforms:

Courrier.configure do |config|
  config.subscriber = {
    provider: "buttondown",
    api_key: "your_api_key"
  }
end
# Add a subscriber
subscriber = Courrier::Subscriber.create "subscriber@example.com"

# Remove a subscriber
subscriber = Courrier::Subscriber.destroy "subscriber@example.com"

if subscriber.success?
  puts "Subscriber added!"
else
  puts "Error: #{subscriber.error}"
end

Supported providers

Provider-specific configuration:

config.subscriber = {
  provider: "mailchimp",
  api_key: "your_api_key",
  dc: "us19",
  list_id: "abc123"
}

Custom providers

Create custom providers by inheriting from Courrier::Subscriber::Base:

class CustomSubscriberProvider < Courrier::Subscriber::Base
  ENDPOINT_URL = "https://api.example.com/subscribers"

  def create(email)
    request(:post, ENDPOINT_URL, {"email" => email})
  end

  def destroy(email)
    request(:delete, "#{ENDPOINT_URL}/#{email}")
  end

  private

  def headers
    {
      "Authorization" => "Bearer #{@api_key}",
      "Content-Type" => "application/json"
    }
  end
end

Then configure it:

config.subscriber = {
  provider: CustomSubscriberProvider,
  api_key: "your_api_key"
}

See existing providers for more examples.

FAQ

Is this for Rails only?

Not at all! Courrier works with any Ruby application. For Rails apps, use rails_courrier.

Can it send using SMTP?

No. Courrier is specifically built for API-based email delivery.

Contributing

This project uses Standard for formatting Ruby code. Please make sure to run rake before submitting pull requests.

License

Courrier is released under the MIT License.