multi_smtp_plus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b7a68d98408ad8a680dc88eff1216c7e0e223a95f89c8e81bb67b27568617a15
4
+ data.tar.gz: 0e5385bcf46760c3bffe44280ba11a5f059812c06e9dac9268dfa82cb960bacf
5
+ SHA512:
6
+ metadata.gz: cfbbda22636aa8c6b70ca16729715068aea67d1530796bf664b886231291751dbe061c1ce10cecca05c8027d80e1b59a76e9e9078dc3fc24116a85dadaa77326
7
+ data.tar.gz: 461308cb9502a676d3b500c41312fbd6954689694440d4673c7be29be3b30adeba6d0f66e4085efc5daa23c65411190815f48539a72320bf873b26139ed5b713
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in multi_smtp.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Harlow Ward
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # MultiSMTP
2
+
3
+ Email delivery is a critical component of many web applications. Occasionally
4
+ third-party services can experience temporary downtime. We can achieve automatic failover by overriding the default email delivery method with MultiSMTP.
5
+
6
+ MultiSMTP takes an array of (1..N) SMTP providers and will iterate over each provider until the email is successfully sent.
7
+
8
+ ## Compatibility
9
+
10
+ - Ruby 3.1–3.3
11
+ - Rails 6.1, 7.x, 8.0+
12
+ - Mail >= 2.7, < 3.0
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "multi_smtp_plus"
20
+ ```
21
+
22
+ Then bundle:
23
+
24
+ ```
25
+ $ bundle
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ Set the delivery method to `:multi_smtp` for each environment that should use
31
+ the automatic failover. Registration is lazy via `ActiveSupport.on_load(:action_mailer)`.
32
+
33
+ ```ruby
34
+ # config/environments/{staging,production}.rb
35
+ Rails.application.configure do
36
+ config.action_mailer.delivery_method = :multi_smtp
37
+ end
38
+ ```
39
+
40
+ In an initializer configure the MultiSMTP class with an array of (1..N) SMTP
41
+ providers.
42
+
43
+ ```ruby
44
+ # config/initializers/multi_smtp.rb
45
+
46
+ # Optional: choose how providers are ordered for each delivery
47
+ # :sequential (default) or :round_robin
48
+ MultiSMTP.rotation_strategy = :round_robin
49
+
50
+ # Optional: provide a state store for cross-process round robin
51
+ # It must respond to `#incr(key)` and return an Integer. Example using Redis:
52
+ # MultiSMTP.state_store = Redis.new
53
+
54
+ sendgrid_settings = {
55
+ address: 'smtp.sendgrid.net',
56
+ authentication: :plain,
57
+ domain: 'example.com',
58
+ password: ENV['SENDGRID_PASSWORD'],
59
+ port: 587,
60
+ user_name: ENV['SENDGRID_USERNAME'],
61
+ # Optional: dynamically skip this provider when a condition is met
62
+ # e.g., free-tier quota exhausted
63
+ skip_if: -> { ENV['SENDGRID_QUOTA_EXHAUSTED'] == '1' }
64
+ }
65
+
66
+ mailgun_settings = {
67
+ address: 'smtp.mailgun.org',
68
+ authentication: :plain,
69
+ domain: 'example.com',
70
+ password: ENV['MAILGUN_PASSWORD'],
71
+ port: 587,
72
+ user_name: ENV['MAILGUN_USERNAME'],
73
+ # Example: skip based on a runtime counter (e.g., Redis)
74
+ # skip_if: -> { Redis.current.get('mailgun:monthly_count').to_i > 990 }
75
+ }
76
+
77
+ MultiSMTP.smtp_providers = [sendgrid_settings, mailgun_settings]
78
+ ```
79
+
80
+ ### Switching between free tiers (never pay for email)
81
+
82
+ Use either or both of the following:
83
+
84
+ - **Round-robin rotation**: spreads deliveries among providers so you consume free quotas evenly.
85
+ - **Per-provider `skip_if`**: a Proc/lambda evaluated before attempting a delivery. Return true to skip a provider (e.g., when you detect free-tier quota is exhausted).
86
+
87
+ For cross-process/app-server rotation, configure `MultiSMTP.state_store` with a Redis-like store that implements `#incr(key)`.
88
+
89
+ ## Error Notifications
90
+
91
+ If all SMTP providers fail the default behavior is to re-raise the original exception.
92
+ However, we can also specify custom notifications.
93
+
94
+ ```ruby
95
+ # config/initializers/multi_smtp.rb
96
+ require "multi_smtp/notifiers/airbrake"
97
+
98
+ MultiSMTP.error_notifier = MultiSMTP::Notifiers::Airbrake
99
+ ```
100
+
101
+ If there is another type of notification you'd like to receive, you can create a
102
+ new notifier that implements the class method `.notify(exception, mail)`.
103
+
104
+ ```ruby
105
+ class MyCustomNotifier
106
+ def self.notify(exception, mail)
107
+ # send to your observability tool, log with context, etc.
108
+ end
109
+ end
110
+
111
+ MultiSMTP.error_notifier = MyCustomNotifier
112
+ ```
113
+
114
+ See the [Airbrake Notifier](lib/multi_smtp/notifiers/airbrake.rb) for more details.
115
+
116
+ ## Migration from <= 0.0.2
117
+
118
+ - Notifier signature changed from `notify(mail)` to `notify(exception, mail)` to provide error context.
119
+ - Added `MultiSMTP.rotation_strategy` and `MultiSMTP.state_store` for provider ordering.
120
+ - Added per-provider `:skip_if` Proc for dynamic skipping (useful for free-tier quotas).
121
+ - Rails registration is now lazy via `ActiveSupport.on_load(:action_mailer)`.
122
+ - Runtime dependency updated to `mail >= 2.7, < 3` for Rails 6–8 compatibility.
123
+
124
+ ## Contributing
125
+
126
+ 1. Fork it ( https://github.com/harlow/multi_smtp/fork )
127
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
128
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
129
+ 4. Push to the branch (`git push origin my-new-feature`)
130
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,78 @@
1
+ require "mail"
2
+
3
+ module MultiSMTP
4
+ class Mail < Mail::SMTP
5
+ def initialize(default_settings)
6
+ @default_settings = default_settings || {}
7
+ super(@default_settings)
8
+ end
9
+
10
+ def deliver!(mail)
11
+ providers = ordered_providers
12
+
13
+ providers.each_with_index do |smtp_provider, index|
14
+ self.settings = default_settings.merge(smtp_provider)
15
+
16
+ begin
17
+ super(mail)
18
+ break
19
+ rescue Exception => e
20
+ next unless all_providers_failed?(index, providers)
21
+
22
+ if error_notifier
23
+ # Pass the exception and the mail to the notifier
24
+ error_notifier.notify(e, mail)
25
+ else
26
+ raise e
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def ordered_providers
35
+ case MultiSMTP.rotation_strategy
36
+ when :round_robin
37
+ rotate_round_robin(MultiSMTP.smtp_providers.dup)
38
+ else
39
+ MultiSMTP.smtp_providers.dup
40
+ end
41
+ end
42
+
43
+ def rotate_round_robin(list)
44
+ return list if list.empty? || list.size == 1
45
+
46
+ # Cross-process if state_store provided, else per-process fallback
47
+ start_idx = if (store = MultiSMTP.state_store)
48
+ store.incr("multi_smtp:rr:index") % list.size
49
+ else
50
+ self.class.__per_process_counter = (self.class.__per_process_counter + 1) % list.size
51
+ end
52
+
53
+ list.rotate(start_idx)
54
+ end
55
+
56
+ def self.__per_process_counter
57
+ @__per_process_counter ||= 0
58
+ end
59
+
60
+ def self.__per_process_counter=(val)
61
+ @__per_process_counter = val
62
+ end
63
+
64
+ def smtp_providers
65
+ @smtp_providers ||= MultiSMTP.smtp_providers
66
+ end
67
+
68
+ def error_notifier
69
+ @error_notifier ||= MultiSMTP.error_notifier
70
+ end
71
+
72
+ def all_providers_failed?(index, providers)
73
+ (providers.size - 1) == index
74
+ end
75
+
76
+ attr_reader :default_settings
77
+ end
78
+ end
@@ -0,0 +1,21 @@
1
+ module MultiSMTP
2
+ module Notifiers
3
+ module Airbrake
4
+ def self.notify(exception, mail)
5
+ ::Airbrake.notify(exception,
6
+ error_message: "Email delivery failed with all SMTP providers.",
7
+ parameters: { mail: extract_mail_params(mail) }
8
+ )
9
+ end
10
+
11
+ def self.extract_mail_params(mail)
12
+ {
13
+ delivery_handler: mail.delivery_handler.to_s,
14
+ from: mail.from,
15
+ subject: mail.subject,
16
+ to: mail.to
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module MultiSMTP
2
+ VERSION = "1.0.0"
3
+ end
data/lib/multi_smtp.rb ADDED
@@ -0,0 +1,51 @@
1
+ require "multi_smtp/mail"
2
+ require "multi_smtp/version"
3
+
4
+ module MultiSMTP
5
+ def self.error_notifier=(notifier)
6
+ @error_notifier = notifier
7
+ end
8
+
9
+ def self.error_notifier
10
+ @error_notifier || false
11
+ end
12
+
13
+ def self.smtp_providers=(providers)
14
+ @smtp_providers = providers
15
+ end
16
+
17
+ def self.smtp_providers
18
+ @smtp_providers || raise("MultiSMTP Error: Please specify smtp_providers.")
19
+ end
20
+
21
+ # Optional rotation strategy for provider ordering. Supported: :sequential (default), :round_robin
22
+ def self.rotation_strategy=(strategy)
23
+ @rotation_strategy = strategy
24
+ end
25
+
26
+ def self.rotation_strategy
27
+ @rotation_strategy || :sequential
28
+ end
29
+
30
+ # Optional state store for cross-process round robin. Must respond to :incr(key) and return an Integer
31
+ # Example: a thin wrapper around Redis INCR. If nil, round_robin falls back to per-process rotation.
32
+ def self.state_store=(store)
33
+ @state_store = store
34
+ end
35
+
36
+ def self.state_store
37
+ @state_store
38
+ end
39
+ end
40
+
41
+ if defined?(Rails)
42
+ begin
43
+ require "active_support"
44
+ ActiveSupport.on_load(:action_mailer) do
45
+ ActionMailer::Base.add_delivery_method(:multi_smtp, MultiSMTP::Mail)
46
+ end
47
+ rescue LoadError
48
+ # Fallback for environments without ActiveSupport loaded early
49
+ ActionMailer::Base.add_delivery_method(:multi_smtp, MultiSMTP::Mail) if defined?(ActionMailer)
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "multi_smtp/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "multi_smtp_plus"
8
+ spec.version = MultiSMTP::VERSION
9
+ spec.authors = ["Harlow Ward", "Sunwoo Yang"]
10
+ spec.email = ["harlow@hward.com", "yangsunwoo@gmail.com"]
11
+ spec.summary = %q{Automatic SMTP email failover in Rails with MultiSMTP}
12
+ spec.description = %q{MultiSMTP Plus provides automatic SMTP failover and rotation across multiple providers for Rails (6–8). It supports sequential or round-robin rotation and per-provider skip conditions (e.g., free-tier quota checks), with optional cross-process state via Redis.}
13
+ spec.homepage = "https://github.com/woosunwoo/multi_smtp"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1"
16
+ spec.metadata = {
17
+ "homepage_uri" => spec.homepage,
18
+ "source_code_uri" => spec.homepage,
19
+ "changelog_uri" => File.join(spec.homepage, "releases")
20
+ }
21
+
22
+ spec.files = `git ls-files -z`.split("\x0")
23
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
+ spec.require_paths = ["lib"]
26
+
27
+ # Mail 2.7–2.8 are used by Rails 6–8
28
+ spec.add_runtime_dependency "mail", ">= 2.7", "< 3"
29
+
30
+ spec.add_development_dependency "bundler", "~> 2.0"
31
+ spec.add_development_dependency "pry", "~> 0.10"
32
+ spec.add_development_dependency "rake", "~> 12.0"
33
+ spec.add_development_dependency "rspec", "~> 3.10"
34
+ end
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ describe MultiSMTP::Mail, "#deliver!" do
4
+ let(:error_notifier) { double(:notifier) }
5
+
6
+ before do
7
+ allow(error_notifier).to receive(:notify)
8
+ MultiSMTP.error_notifier = error_notifier
9
+ MultiSMTP.smtp_providers = [
10
+ provider("smtp.sendgrid.net", "sendgrid"),
11
+ provider("smtp.amazaon_ses.com", "amazon_ses")
12
+ ]
13
+ end
14
+
15
+ context "primary email provider succeeds" do
16
+ it "sends an email" do
17
+ allow_any_instance_of(Net::SMTP).to receive(:start).and_return(true)
18
+
19
+ MultiSMTP::Mail.new({}).deliver!(mail)
20
+
21
+ expect(error_notifier).not_to have_received(:notify)
22
+ end
23
+ end
24
+
25
+ context "primary email provider fails" do
26
+ it "sends an email with secondary provider" do
27
+ allow_any_instance_of(Net::SMTP).to receive(:start).
28
+ with("test.com", "sendgrid", "password", :login).
29
+ and_raise(Net::SMTPFatalError)
30
+
31
+ allow_any_instance_of(Net::SMTP).to receive(:start).
32
+ with("test.com", "amazon_ses", "password", :login).
33
+ and_return(true)
34
+
35
+ MultiSMTP::Mail.new({}).deliver!(mail)
36
+
37
+ expect(error_notifier).to_not have_received(:notify)
38
+ end
39
+ end
40
+
41
+ context "all smtp providers fail" do
42
+ context "notifier is set" do
43
+ it "notifies airbrake" do
44
+ allow_any_instance_of(Net::SMTP).to receive(:start).
45
+ and_raise(Net::SMTPFatalError)
46
+
47
+ MultiSMTP::Mail.new({}).deliver!(mail)
48
+
49
+ expect(error_notifier).to have_received(:notify).once
50
+ end
51
+ end
52
+
53
+ context "notifier is not set" do
54
+ it "re-raises the original exception" do
55
+ MultiSMTP.error_notifier = nil
56
+ allow_any_instance_of(Net::SMTP).to receive(:start).
57
+ and_raise(Net::SMTPFatalError)
58
+
59
+ expect { MultiSMTP::Mail.new({}).deliver!(mail) }.to raise_exception
60
+ end
61
+ end
62
+ end
63
+
64
+ def mail
65
+ Mail.new(
66
+ delivery_handler: "test",
67
+ from: "from@test.com",
68
+ subject: "test",
69
+ to: "to@test.com"
70
+ )
71
+ end
72
+
73
+ def provider(address, user_name)
74
+ {
75
+ address: address,
76
+ authentication: :login,
77
+ domain: "test.com",
78
+ password: "password",
79
+ user_name: user_name
80
+ }
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe MultiSMTP, ".smtp_providers" do
4
+ it "returns an array of providers" do
5
+ MultiSMTP.smtp_providers = [:provider1, :provider2]
6
+
7
+ result = MultiSMTP.smtp_providers
8
+
9
+ expect(result).to eq [:provider1, :provider2]
10
+ end
11
+
12
+ context "no providers set" do
13
+ it "raises and exception" do
14
+ expect { MultiSMTP.smtp_providers }.to raise_exception
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), "..", "lib")
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__))
3
+
4
+ require "rubygems"
5
+ require "rspec"
6
+ require "pry"
7
+ require "multi_smtp"
8
+
9
+ RSpec.configure do |config|
10
+ config.after :each do
11
+ MultiSMTP.smtp_providers = nil
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multi_smtp_plus
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Harlow Ward
8
+ - Sunwoo Yang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-08-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mail
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '2.7'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '3'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '2.7'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ - !ruby/object:Gem::Dependency
35
+ name: bundler
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ type: :development
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: pry
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '12.0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '12.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: rspec
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.10'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.10'
90
+ description: MultiSMTP Plus provides automatic SMTP failover and rotation across multiple
91
+ providers for Rails (6–8). It supports sequential or round-robin rotation and per-provider
92
+ skip conditions (e.g., free-tier quota checks), with optional cross-process state
93
+ via Redis.
94
+ email:
95
+ - harlow@hward.com
96
+ - yangsunwoo@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - ".gitignore"
102
+ - Gemfile
103
+ - LICENSE.txt
104
+ - README.md
105
+ - Rakefile
106
+ - lib/multi_smtp.rb
107
+ - lib/multi_smtp/mail.rb
108
+ - lib/multi_smtp/notifiers/airbrake.rb
109
+ - lib/multi_smtp/version.rb
110
+ - multi_smtp.gemspec
111
+ - spec/lib/multi_smtp/mail_spec.rb
112
+ - spec/lib/multi_smtp_spec.rb
113
+ - spec/spec_helper.rb
114
+ homepage: https://github.com/woosunwoo/multi_smtp
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ homepage_uri: https://github.com/woosunwoo/multi_smtp
119
+ source_code_uri: https://github.com/woosunwoo/multi_smtp
120
+ changelog_uri: https://github.com/woosunwoo/multi_smtp/releases
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '3.1'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.4.10
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: Automatic SMTP email failover in Rails with MultiSMTP
140
+ test_files:
141
+ - spec/lib/multi_smtp/mail_spec.rb
142
+ - spec/lib/multi_smtp_spec.rb
143
+ - spec/spec_helper.rb