Skip to content

Rails-Designer/courrier

Repository files navigation

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
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
  1. Email class defaults
class OrderEmail < Courrier::Email
  configure from: "orders@railsdesigner.com",
            cc: "records@railsdesigner.com",
            provider: "mailgun"
end
  1. Instance options
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.

About

API-powered email delivery for Ruby apps. With support for Mailgun, Postmark, Resend and more

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Contributors