diff --git a/README.md b/README.md index c438c0d..ea3b4fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Courrier -API-powered email delivery for Ruby apps. +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](https://raw.githubusercontent.com/Rails-Designer/courrier/HEAD/.github/cover.jpg) @@ -15,6 +15,9 @@ class OrderEmail < Courrier::Email end OrderEmail.deliver to: "recipient@railsdesigner.com" + +# Manage newsletter subscriptions +Courrier::Subscriber.create "subscriber@example.com" ``` @@ -80,8 +83,11 @@ Courrier uses a configuration system with three levels (from lowest to highest p 1. **Global configuration** ```ruby Courrier.configure do |config| - config.provider = "postmark" - config.api_key = "xyz" + config.email = { + provider: "postmark", + api_key: "xyz" + } + config.from = "devs@railsdesigner.com" config.default_url_options = { host: "railsdesigner.com" } @@ -112,7 +118,7 @@ OrderEmail.deliver to: "recipient@railsdesigner.com",\ 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 Attributes +## 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: ```ruby @@ -130,12 +136,12 @@ end ``` -## Result Object +## 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 +### Available methods | Method | Return Type | Description | |:-------|:-----------|:------------| @@ -176,7 +182,7 @@ Courrier supports these transactional email providers: Additional functionality to help with development and testing: -### Background Jobs (Rails only) +### Background jobs (Rails only) Use `deliver_later` to enqueue delivering using Rails' ActiveJob. You can set various ActiveJob-supported options in the email class, like so: `enqueue queue: "emails", wait: 5.minutes`. @@ -206,7 +212,7 @@ config.inbox.auto_open = true Emails are automatically cleared with `bin/rails tmp:clear`, or manually with `bin/rails courrier:clear`. -### Layout Support +### Layout support Wrap your email content using layouts: ```ruby @@ -249,7 +255,7 @@ end ``` -### Auto-generate Text from HTML +### Auto-generate text from HTML Automatically generate plain text versions from your HTML emails: ```ruby @@ -257,7 +263,7 @@ config.auto_generate_text = true # Defaults to false ``` -### Email Address Helper +### Email address helper Compose email addresses with display names: ```ruby @@ -284,16 +290,17 @@ end ``` -### Logger Provider +### Logger provider Use Ruby's built-in Logger for development and testing: ```ruby -config.provider = "logger" # Outputs emails to STDOUT -config.logger = custom_logger # Optional: defaults to ::Logger.new($stdout) +config.provider = "logger" # outputs emails to STDOUT +config.logger = custom_logger # optional: defaults to ::Logger.new($stdout) ``` -### Custom Providers + +### Custom providers Create your own provider by inheriting from `Courrier::Email::Providers::Base`: ```ruby @@ -314,6 +321,89 @@ config.provider = "CustomProvider" Check the [existing providers](https://github.com/Rails-Designer/courrier/tree/main/lib/courrier/email/providers) for implementation examples. +## Newsletter subscriptions + +Manage subscribers across popular email marketing platforms: +```ruby +Courrier.configure do |config| + config.subscriber = { + provider: "buttondown", + api_key: "your_api_key" + } +end +``` + +```ruby +# 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 + +- [Beehiiv](https://www.beehiiv.com/) - requires `publication_id` +- [Buttondown](https://buttondown.com) +- [Kit](https://kit.com/) (formerly ConvertKit) - requires `form_id` +- [Loops](https://loops.so/) +- [Mailchimp](https://mailchimp.com/) - requires `dc` and `list_id` +- [MailerLite](https://www.mailerlite.com/) + +Provider-specific configuration: +```ruby +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`: +```ruby +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: +```ruby +config.subscriber = { + provider: CustomSubscriberProvider, + api_key: "your_api_key" +} +``` + +See [existing providers](https://github.com/Rails-Designer/courrier/tree/main/lib/courrier/subscriber) for more examples. + + ## FAQ ### Is this a replacement for ActionMailer? diff --git a/lib/courrier.rb b/lib/courrier.rb index 39f873d..5bf5ed7 100644 --- a/lib/courrier.rb +++ b/lib/courrier.rb @@ -4,6 +4,7 @@ require "courrier/errors" require "courrier/configuration" require "courrier/email" +require "courrier/subscriber" require "courrier/engine" if defined?(Rails) require "courrier/railtie" if defined?(Rails) diff --git a/lib/courrier/configuration.rb b/lib/courrier/configuration.rb index 5ab7b2e..4411fce 100644 --- a/lib/courrier/configuration.rb +++ b/lib/courrier/configuration.rb @@ -19,13 +19,15 @@ def configuration end class Configuration - attr_accessor :provider, :api_key, :logger, :email_path, :layouts, :default_url_options, :auto_generate_text, + attr_accessor :email, :subscriber, :logger, :email_path, :layouts, :default_url_options, :auto_generate_text, :from, :reply_to, :cc, :bcc + attr_reader :providers, :inbox def initialize - @provider = "logger" - @api_key = nil + @email = {provider: "logger"} + @subscriber = {} + @logger = ::Logger.new($stdout) @email_path = default_email_path @@ -42,6 +44,30 @@ def initialize @inbox = Courrier::Configuration::Inbox.new end + def provider + warn "[DEPRECATION] `provider` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0" + + @email[:provider] + end + + def provider=(value) + warn "[DEPRECATION] `provider=` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0" + + @email[:provider] = value + end + + def api_key + warn "[DEPRECATION] `api_key` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0" + + @email[:api_key] + end + + def api_key=(value) + warn "[DEPRECATION] `api_key=` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0" + + @email[:api_key] = value + end + private def default_email_path diff --git a/lib/courrier/email.rb b/lib/courrier/email.rb index 27d7a09..5fe751e 100644 --- a/lib/courrier/email.rb +++ b/lib/courrier/email.rb @@ -17,7 +17,7 @@ class << self define_method(attribute) do instance_variable_get("@#{attribute}") || (superclass.respond_to?(attribute) ? superclass.send(attribute) : nil) || - Courrier.configuration&.send(attribute) + (["provider", "api_key"].include?(attribute) ? Courrier.configuration&.email&.[](attribute.to_sym) : Courrier.configuration&.send(attribute)) end define_method("#{attribute}=") do |value| @@ -66,8 +66,8 @@ def inherited(subclass) end def initialize(options = {}) - @provider = options[:provider] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.provider - @api_key = options[:api_key] || ENV["COURRIER_API_KEY"] || self.class.api_key || Courrier.configuration&.api_key + @provider = options[:provider] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.email&.[](:provider) + @api_key = options[:api_key] || ENV["COURRIER_API_KEY"] || self.class.api_key || Courrier.configuration&.email&.[](:api_key) @default_url_options = self.class.default_url_options.merge(options[:default_url_options] || {}) @context_options = options.except(:provider, :api_key, :from, :to, :reply_to, :cc, :bcc, :subject, :text, :html) diff --git a/lib/courrier/email/provider.rb b/lib/courrier/email/provider.rb index 9a5fef4..7f08f1b 100644 --- a/lib/courrier/email/provider.rb +++ b/lib/courrier/email/provider.rb @@ -33,6 +33,7 @@ class Provider def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {}) @provider = provider @api_key = api_key + @options = options @provider_options = provider_options @context_options = context_options @@ -40,7 +41,7 @@ def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, c def deliver raise Courrier::ConfigurationError, "`provider` and `api_key` must be configured for production environment" if configuration_missing_in_production? - raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.nil? || @provider.empty? + raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.nil? || @provider.to_s.strip.empty? provider_class.new( api_key: @api_key, diff --git a/lib/courrier/subscriber.rb b/lib/courrier/subscriber.rb new file mode 100644 index 0000000..4e1532a --- /dev/null +++ b/lib/courrier/subscriber.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Courrier + class Subscriber + class << self + def create(email) + provider.create(email) + end + alias_method :add, :create + + def destroy(email) + provider.destroy(email) + end + alias_method :delete, :destroy + + private + + def provider + @provider ||= provider_class.new( + api_key: Courrier.configuration.subscriber[:api_key] + ) + end + + def provider_class + provider_name = Courrier.configuration.subscriber[:provider] + + return provider_name if provider_name.is_a?(Class) + require "courrier/subscriber/#{provider_name}" + + Object.const_get("Courrier::Subscriber::#{provider_name.capitalize}") + end + end + end +end diff --git a/lib/courrier/subscriber/base.rb b/lib/courrier/subscriber/base.rb new file mode 100644 index 0000000..886b930 --- /dev/null +++ b/lib/courrier/subscriber/base.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "courrier/subscriber/result" + +module Courrier + class Subscriber + class Base + def initialize(api_key:) + @api_key = api_key + end + + def create(email) + raise NotImplementedError + end + + def destroy(email) + raise NotImplementedError + end + + private + + def request(method, url, body = nil) + uri = URI(url) + request_class = case method + when :post then Net::HTTP::Post + when :delete then Net::HTTP::Delete + when :put then Net::HTTP::Put + when :patch then Net::HTTP::Patch + when :get then Net::HTTP::Get + end + + request = request_class.new(uri) + request.body = body.to_json if body + + headers.each { |key, value| request[key] = value } + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(request) + end + + Courrier::Subscriber::Result.new(response: response) + rescue => error + Courrier::Subscriber::Result.new(error: error) + end + + def headers + raise NotImplementedError + end + end + end +end diff --git a/lib/courrier/subscriber/beehiiv.rb b/lib/courrier/subscriber/beehiiv.rb new file mode 100644 index 0000000..836604b --- /dev/null +++ b/lib/courrier/subscriber/beehiiv.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Beehiiv < Base + ENDPOINT_URL = "https://api.beehiiv.com/v2/publications" + + def create(email) + publication_id = Courrier.configuration.subscriber[:publication_id] + raise Courrier::ConfigurationError, "Beehiiv requires `publication_id` in subscriber configuration" unless publication_id + + request(:post, "#{ENDPOINT_URL}/#{publication_id}/subscriptions", {"email" => email}) + end + + def destroy(email) + publication_id = Courrier.configuration.subscriber[:publication_id] + raise Courrier::ConfigurationError, "Beehiiv requires `publication_id` in subscriber configuration" unless publication_id + + subscription_id = subscription_id(publication_id, email) + return Courrier::Subscriber::Result.new(error: StandardError.new("Subscription not found")) unless subscription_id + + request(:delete, "#{ENDPOINT_URL}/#{publication_id}/subscriptions/#{subscription_id}") + end + + private + + def subscription_id(publication_id, email) + response = request(:get, "#{ENDPOINT_URL}/#{publication_id}/subscriptions?email=#{email}") + + return nil unless response.success? + + response.data.dig("data", 0, "id") + end + + def headers + { + "Authorization" => "Bearer #{@api_key}", + "Content-Type" => "application/json" + } + end + end + end +end diff --git a/lib/courrier/subscriber/buttondown.rb b/lib/courrier/subscriber/buttondown.rb new file mode 100644 index 0000000..f3d769f --- /dev/null +++ b/lib/courrier/subscriber/buttondown.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Buttondown < Base + ENDPOINT_URL = "https://api.buttondown.email/v1/subscribers" + + def create(email) + request(:post, ENDPOINT_URL, {"email" => email}) + end + + def destroy(email) + request(:delete, "#{ENDPOINT_URL}/#{email}") + end + + private + + def headers + { + "Authorization" => "Token #{@api_key}", + "Content-Type" => "application/json" + } + end + end + end +end diff --git a/lib/courrier/subscriber/kit.rb b/lib/courrier/subscriber/kit.rb new file mode 100644 index 0000000..4383408 --- /dev/null +++ b/lib/courrier/subscriber/kit.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Kit < Base + ENDPOINT_URL = "https://api.convertkit.com/v3/forms" + + def create(email) + form_id = Courrier.configuration.subscriber[:form_id] + raise Courrier::ConfigurationError, "Kit requires `form_id` in subscriber configuration" unless form_id + + request(:post, "#{ENDPOINT_URL}/#{form_id}/subscribe", { + "api_key" => @api_key, + "email" => email + }) + end + + def destroy(email) + request(:put, "https://api.convertkit.com/v3/unsubscribe", { + "api_secret" => @api_key, + "email" => email + }) + end + + private + + def headers + { + "Content-Type" => "application/json" + } + end + end + end +end diff --git a/lib/courrier/subscriber/loops.rb b/lib/courrier/subscriber/loops.rb new file mode 100644 index 0000000..ca6ad5c --- /dev/null +++ b/lib/courrier/subscriber/loops.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Loops < Base + ENDPOINT_URL = "https://app.loops.so/api/v1/contacts" + + def create(email) + request(:post, "#{ENDPOINT_URL}/create", { + "email" => email + }) + end + + def destroy(email) + request(:post, "#{ENDPOINT_URL}/delete", { + "email" => email + }) + end + + private + + def headers + { + "Authorization" => "Bearer #{@api_key}", + "Content-Type" => "application/json" + } + end + end + end +end diff --git a/lib/courrier/subscriber/mailchimp.rb b/lib/courrier/subscriber/mailchimp.rb new file mode 100644 index 0000000..acd707a --- /dev/null +++ b/lib/courrier/subscriber/mailchimp.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Mailchimp < Base + def create(email) + dc = Courrier.configuration.subscriber[:dc] + list_id = Courrier.configuration.subscriber[:list_id] + + raise Courrier::ConfigurationError, "Mailchimp requires `dc` and `list_id` in subscriber configuration" unless dc && list_id + + request(:post, "https://#{dc}.api.mailchimp.com/3.0/lists/#{list_id}/members", { + "email_address" => email, + "status" => "subscribed" + }) + end + + def destroy(email) + dc = Courrier.configuration.subscriber[:dc] + list_id = Courrier.configuration.subscriber[:list_id] + + raise Courrier::ConfigurationError, "Mailchimp requires `dc` and `list_id` in subscriber configuration" unless dc && list_id + + request(:delete, "https://#{dc}.api.mailchimp.com/3.0/lists/#{list_id}/members/#{email}") + end + + private + + def headers + { + "Authorization" => "Bearer #{@api_key}", + "Content-Type" => "application/json" + } + end + end + end +end diff --git a/lib/courrier/subscriber/mailerlite.rb b/lib/courrier/subscriber/mailerlite.rb new file mode 100644 index 0000000..5f53b28 --- /dev/null +++ b/lib/courrier/subscriber/mailerlite.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "courrier/subscriber/base" + +module Courrier + class Subscriber + class Mailerlite < Base + ENDPOINT_URL = "https://connect.mailerlite.com/api/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 + end +end diff --git a/lib/courrier/subscriber/result.rb b/lib/courrier/subscriber/result.rb new file mode 100644 index 0000000..a55f2b0 --- /dev/null +++ b/lib/courrier/subscriber/result.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Courrier + class Subscriber + class Result + attr_reader :success, :response, :data, :error + + def initialize(response: nil, error: nil) + @response = response + @error = error + @data = parsed(@response&.body) + @success = successful? + end + + def success? = @success + + private + + def parsed(body) + return {} if @response.nil? + + begin + JSON.parse(body) + rescue JSON::ParserError + {} + end + end + + def successful? + return false if response_failed? + return @data["success"] if @data.key?("success") + + (200..299).cover?(status_code) + end + + def response_failed? = @error || @response.nil? + + def status_code = @response.code.to_i + end + end +end diff --git a/lib/generators/courrier/templates/initializer.rb.tt b/lib/generators/courrier/templates/initializer.rb.tt index 1c7b591..7f42ab9 100644 --- a/lib/generators/courrier/templates/initializer.rb.tt +++ b/lib/generators/courrier/templates/initializer.rb.tt @@ -1,12 +1,17 @@ Courrier.configure do |config| include Courrier::Email::Address - # Choose your email delivery provider - # Default: `logger` - # config.provider = "" # choose from: <%= Courrier::Email::Provider::PROVIDERS.keys.join(", ") %> + # Set your email delivery provider + # config.email = { + # provider = "", # default, `logger`, choose from: <%= Courrier::Email::Provider::PROVIDERS.keys.join(", ") %> + # api_key = "" your transactional email provider's API key + } + + # Set your marketing email provider + # config.subscriber = { + # provider = "" + } - # Add your email provider's API key - # config.api_key = "" # Configure provider-specific settings # config.providers.loops.transactional_id = "" diff --git a/test/courrier/email_configuration_test.rb b/test/courrier/email_configuration_test.rb index 770fd5f..0bcacfc 100644 --- a/test/courrier/email_configuration_test.rb +++ b/test/courrier/email_configuration_test.rb @@ -31,8 +31,10 @@ def test_initialization_with_class_defaults def test_initialization_with_configuration_defaults Courrier.configure do |config| - config.provider = "config_provider" - config.api_key = "config_key" + config.email = { + provider: "config_provider", + api_key: "config_key" + } config.from = "devs@railsdesigner.com" config.reply_to = "config_reply@railsdesigner.com" config.cc = "config_cc@railsdesigner.com" @@ -67,8 +69,7 @@ def test_instance_options_override_class_defaults end def test_class_defaults_override_configuration - Courrier.configure { _1.provider = "config_provider" } - + Courrier.configure { _1.email = {provider: "config_provider" }} TestEmail.configure(provider: "class_provider") email = TestEmail.new(from: "devs@railsdesigner.com", to: "recipient@railsdesigner.com") @@ -77,8 +78,7 @@ def test_class_defaults_override_configuration end def test_class_defaults_set_configuration - Courrier.configure { _1.provider = "config_provider" } - + Courrier.configure { _1.email = {provider: "config_provider" }} TestEmail.set(provider: "class_provider") email = TestEmail.new(from: "devs@railsdesigner.com", to: "recipient@railsdesigner.com") @@ -87,7 +87,7 @@ def test_class_defaults_set_configuration end def test_configuration_used_when_no_class_defaults - Courrier.configure { _1.provider = "config_provider" } + Courrier.configure { _1.email = {provider: "config_provider" }} email = TestEmail.new(from: "devs@railsdesigner.com", to: "recipient@railsdesigner.com") diff --git a/test/courrier/subscriber/result_test.rb b/test/courrier/subscriber/result_test.rb new file mode 100644 index 0000000..4df16d3 --- /dev/null +++ b/test/courrier/subscriber/result_test.rb @@ -0,0 +1,47 @@ +require "test_helper" + +require "courrier/subscriber/result" + +class Courrier::Subscriber::ResultTest < Minitest::Test + def test_initialize_with_success_response + response = Data.define(:code, :body).new( + code: "200", + body: '{"id": "sub-123", "email": "test@example.com"}' + ) + result = Courrier::Subscriber::Result.new(response: response) + + assert result.success? + assert_equal({"id" => "sub-123", "email" => "test@example.com"}, result.data) + end + + def test_initialize_with_failure_response + response = Data.define(:code, :body).new( + code: "400", + body: '{"error": "Invalid email"}' + ) + result = Courrier::Subscriber::Result.new(response: response) + + refute result.success? + assert_equal({"error" => "Invalid email"}, result.data) + end + + def test_initialize_with_error + error = StandardError.new("Connection error") + result = Courrier::Subscriber::Result.new(error: error) + + refute result.success? + assert_equal({}, result.data) + end + + def test_success_predicate_method + result = Courrier::Subscriber::Result.new( + response: Data.define(:code, :body).new(code: "200", body: "{}") + ) + assert result.success? + + result = Courrier::Subscriber::Result.new( + response: Data.define(:code, :body).new(code: "500", body: "{}") + ) + refute result.success? + end +end diff --git a/test/courrier/subscriber_test.rb b/test/courrier/subscriber_test.rb new file mode 100644 index 0000000..929281b --- /dev/null +++ b/test/courrier/subscriber_test.rb @@ -0,0 +1,72 @@ +require "test_helper" + +require "courrier/subscriber" +require "courrier/subscriber/result" + +class Courrier::SubscriberTest < Minitest::Test + def setup + reset_configuration + end + + def test_create_with_configured_provider + Courrier.configure do |config| + config.subscriber = { + provider: :buttondown, + api_key: "test_key" + } + end + + provider_mock = Minitest::Mock.new + provider_mock.expect :create, Courrier::Subscriber::Result.new(response: success_response), ["test@example.com"] + + Courrier::Subscriber.stub :provider, provider_mock do + result = Courrier::Subscriber.create("test@example.com") + assert result.success? + end + + provider_mock.verify + end + + def test_destroy_with_configured_provider + Courrier.configure do |config| + config.subscriber = { + provider: :buttondown, + api_key: "test_key" + } + end + + provider_mock = Minitest::Mock.new + provider_mock.expect :destroy, Courrier::Subscriber::Result.new(response: success_response), ["test@example.com"] + + Courrier::Subscriber.stub :provider, provider_mock do + result = Courrier::Subscriber.destroy("test@example.com") + assert result.success? + end + + provider_mock.verify + end + + def test_add_alias_for_create + assert_equal Courrier::Subscriber.method(:create), Courrier::Subscriber.method(:add) + end + + def test_delete_alias_for_destroy + assert_equal Courrier::Subscriber.method(:destroy), Courrier::Subscriber.method(:delete) + end + + private + + def success_response + Data.define(:code, :body).new(code: "200", body: '{"success": true}') + end + + def reset_configuration + Courrier.configuration = nil + Courrier.configure do |config| + config.subscriber = { + provider: nil, + subscriber_api_key: nil + } + end + end +end