chain_mail 0.1.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 +7 -0
- data/.pre-commit-config.yaml +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +24 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/Rakefile +12 -0
- data/assets/images/logo.png +0 -0
- data/chain_mail.gemspec +38 -0
- data/config/initializers/chain_mail.rb +16 -0
- data/lib/chain_mail/configuration.rb +23 -0
- data/lib/chain_mail/delivery.rb +107 -0
- data/lib/chain_mail/providers/base.rb +28 -0
- data/lib/chain_mail/providers/brevo.rb +23 -0
- data/lib/chain_mail/providers/mailgun.rb +55 -0
- data/lib/chain_mail/providers/one_signal.rb +21 -0
- data/lib/chain_mail/providers/postmark.rb +21 -0
- data/lib/chain_mail/providers/send_grid.rb +29 -0
- data/lib/chain_mail/providers/send_pulse.rb +90 -0
- data/lib/chain_mail/providers/ses.rb +25 -0
- data/lib/chain_mail/railtie.rb +11 -0
- data/lib/chain_mail/version.rb +5 -0
- data/lib/chain_mail.rb +58 -0
- data/sig/chain_mail.rbs +4 -0
- data/vendor/sendpulse/sendpulse_api.rb +656 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5f26cf6af379b9e04d3c78dc39cd1b47a81905b66e4acee3a1cb0b9d13a16517
|
4
|
+
data.tar.gz: d39eb11222bff40e7e6de131dae95ea2c47a6dbfd0001b36b491507ee4808b25
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c5d66fe44f1f6daeedc9c0a5efb7519adb7eb03be921756cd137b63db67584b9d929001fdd7b05ec2e78211c67a01b1187530bff27e9aa1ceaa117f96736b177
|
7
|
+
data.tar.gz: 0b072a3d2ddef79ca2a3427c9162e6f9280bf5b7ad314fc4dd9d6e124d932a51de2ab72d5894dde8f4601f3eda2ade59efa64e3019188248c0e155ceeca0dc27
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.1
|
3
|
+
NewCops: enable
|
4
|
+
Exclude:
|
5
|
+
- "db/**/*"
|
6
|
+
- "node_modules/**/*"
|
7
|
+
- "vendor/**/*"
|
8
|
+
|
9
|
+
Layout/LineLength:
|
10
|
+
Max: 100
|
11
|
+
|
12
|
+
Metrics/BlockLength:
|
13
|
+
Exclude:
|
14
|
+
- "spec/**/*"
|
15
|
+
- "chain_mail.gemspec"
|
16
|
+
|
17
|
+
Metrics/MethodLength:
|
18
|
+
Max: 35
|
19
|
+
|
20
|
+
Style/StringLiterals:
|
21
|
+
EnforcedStyle: double_quotes
|
22
|
+
|
23
|
+
Style/Documentation:
|
24
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 T
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
# ChainMail
|
4
|
+
|
5
|
+
[](https://coveralls.io/github/taltas/chain_mail?branch=main)
|
6
|
+
[](https://www.ruby-lang.org)
|
7
|
+
[](https://github.com/taltas/chain_mail/actions)
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
9
|
+
[](https://github.com/rubocop/rubocop)
|
10
|
+
|
11
|
+
ChainMail is a Ruby gem that ensures your transactional **emails never fail** by automatically switching between multiple email providers (SendGrid, Postmark, Mailgun, SES, etc.) when one fails to send. No more lost emails, no more manual intervention required.
|
12
|
+
|
13
|
+
## Why ChainMail?
|
14
|
+
|
15
|
+
- **Zero Downtime**: If one provider fails, emails automatically route to the next available provider
|
16
|
+
- **Easy Setup**: Simple configuration with familiar Rails patterns
|
17
|
+
- **Multiple Providers**: Built-in support for all major email services
|
18
|
+
- **Error Aggregation**: Get detailed reports on any delivery issues
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'chain_mail'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle install
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install chain_mail
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Rails Setup
|
39
|
+
|
40
|
+
Add a configuration initializer at [`config/initializers/chain_mail.rb`](config/initializers/chain_mail.rb):
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
ChainMail.configure do |config|
|
44
|
+
config.providers = [
|
45
|
+
{ send_grid: { api_key: ENV["SENDGRID_API_KEY"] } },
|
46
|
+
{ mailgun: { domain: ENV["MAILGUN_DOMAIN"], api_key: ENV["MAILGUN_API_KEY"] } },
|
47
|
+
# Add more providers as needed
|
48
|
+
]
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Set the delivery method in your environment config (e.g. `config/environments/production.rb`):
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
config.action_mailer.delivery_method = :chain_mail
|
56
|
+
```
|
57
|
+
|
58
|
+
Send an email using ActionMailer:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class UserMailer < ApplicationMailer
|
62
|
+
def welcome_email(user)
|
63
|
+
mail(
|
64
|
+
to: user.email,
|
65
|
+
from: 'noreply@example.com',
|
66
|
+
subject: 'Welcome!',
|
67
|
+
body: 'Hello and welcome!'
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
## Requirements
|
74
|
+
|
75
|
+
- Ruby 3.0 or higher
|
76
|
+
- Rails 6.0+ (for Rails integration)
|
77
|
+
- Active email provider accounts (SendGrid, Postmark, etc.)
|
78
|
+
|
79
|
+
## Architecture
|
80
|
+
|
81
|
+
- **Configuration:** Set up providers and credentials in an initializer or before sending.
|
82
|
+
- **Delivery:** Handles failover, input validation, and error aggregation.
|
83
|
+
- **Providers:** Each adapter implements a standardized interface and error handling.
|
84
|
+
|
85
|
+
## Supported Email Providers
|
86
|
+
|
87
|
+
ChainMail includes built-in support for the following email providers. Here's how to configure each one in your Rails initializer:
|
88
|
+
|
89
|
+
### Amazon SES
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
ChainMail.configure do |config|
|
93
|
+
config.providers = [
|
94
|
+
{ ses: {
|
95
|
+
region: ENV["AWS_REGION"],
|
96
|
+
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
97
|
+
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
|
98
|
+
} }
|
99
|
+
]
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### Brevo (formerly Sendinblue)
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
ChainMail.configure do |config|
|
107
|
+
config.providers = [
|
108
|
+
{ brevo: {
|
109
|
+
api_key: ENV["BREVO_API_KEY"],
|
110
|
+
sandbox: ENV["BREVO_SANDBOX"] == "true" # optional
|
111
|
+
} }
|
112
|
+
]
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Mailgun
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
ChainMail.configure do |config|
|
120
|
+
config.providers = [
|
121
|
+
{ mailgun: {
|
122
|
+
domain: ENV["MAILGUN_DOMAIN"],
|
123
|
+
api_key: ENV["MAILGUN_API_KEY"]
|
124
|
+
} }
|
125
|
+
]
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### OneSignal
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
ChainMail.configure do |config|
|
133
|
+
config.providers = [
|
134
|
+
{ one_signal: { api_key: ENV["ONESIGNAL_API_KEY"] } }
|
135
|
+
]
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
### Postmark
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
ChainMail.configure do |config|
|
143
|
+
config.providers = [
|
144
|
+
{ postmark: { api_key: ENV["POSTMARK_API_KEY"] } }
|
145
|
+
]
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
### SendGrid
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
ChainMail.configure do |config|
|
153
|
+
config.providers = [
|
154
|
+
{ send_grid: { api_key: ENV["SENDGRID_API_KEY"] } }
|
155
|
+
]
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### SendPulse
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
ChainMail.configure do |config|
|
163
|
+
config.providers = [
|
164
|
+
{ send_pulse: {
|
165
|
+
client_id: ENV["SENDPULSE_CLIENT_ID"],
|
166
|
+
client_secret: ENV["SENDPULSE_CLIENT_SECRET"]
|
167
|
+
} }
|
168
|
+
]
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
### Multiple Providers by Priority (send_grid -> mailgun -> ses)
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
ChainMail.configure do |config|
|
176
|
+
config.providers = [
|
177
|
+
{ send_grid: { api_key: ENV["SENDGRID_API_KEY"]} },
|
178
|
+
{ mailgun: {
|
179
|
+
domain: ENV["MAILGUN_DOMAIN"],
|
180
|
+
api_key: ENV["MAILGUN_API_KEY"]
|
181
|
+
} },
|
182
|
+
{ ses: {
|
183
|
+
region: ENV["AWS_REGION"],
|
184
|
+
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
185
|
+
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
|
186
|
+
} }
|
187
|
+
]
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
**Note:** Always store API keys and credentials securely using environment variables. Providers are tried in the order listed - the first available provider will handle the email delivery.
|
192
|
+
|
193
|
+
## Provider Priorities & Dynamic Configuration
|
194
|
+
|
195
|
+
- Providers are tried in the order listed in `config.providers`.
|
196
|
+
- You can register/unregister adapters at runtime:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
ChainMail.register_provider(:custom, CustomProviderClass)
|
200
|
+
ChainMail.unregister_provider(:send_grid)
|
201
|
+
```
|
202
|
+
|
203
|
+
- You can update provider priorities or credentials dynamically:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
ChainMail.config.providers = [
|
207
|
+
{ custom: { api_key: "CUSTOM_API_KEY" } },
|
208
|
+
{ mailgun: { domain: "MAILGUN_DOMAIN", api_key: "MAILGUN_API_KEY" } }
|
209
|
+
]
|
210
|
+
```
|
211
|
+
|
212
|
+
- API keys and credentials should be stored securely, e.g. using environment variables.
|
213
|
+
|
214
|
+
## Development
|
215
|
+
|
216
|
+
Check out the repo and start developing!
|
217
|
+
|
218
|
+
## Contributing
|
219
|
+
|
220
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/taltas/chain_mail.
|
data/Rakefile
ADDED
Binary file
|
data/chain_mail.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/chain_mail/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "chain_mail"
|
7
|
+
spec.version = ChainMail::VERSION
|
8
|
+
spec.authors = ["Toray Altas"]
|
9
|
+
spec.email = ["toray.altas@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Unified transactional email delivery with failover for Ruby/Rails."
|
12
|
+
spec.description =
|
13
|
+
"ChainMail provides a unified interface for sending transactional emails through " \
|
14
|
+
"multiple providers (SendGrid, Postmark, Mailgun, etc.) " \
|
15
|
+
"with automatic failover and Rails integration."
|
16
|
+
spec.homepage = "https://github.com/taltas/chain_mail"
|
17
|
+
spec.required_ruby_version = ">= 3.0.0"
|
18
|
+
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/taltas/chain_mail"
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/taltas/chain_mail/blob/main/CHANGELOG.md"
|
23
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
24
|
+
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .github
|
28
|
+
appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
# Add dependencies here
|
36
|
+
spec.add_dependency "aws-sdk-ses", "~> 1.0"
|
37
|
+
spec.add_dependency "rails", ">= 6.0"
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ChainMail configuration initializer for Rails
|
4
|
+
|
5
|
+
# Configure your email providers and credentials here.
|
6
|
+
ChainMail.configure do |config|
|
7
|
+
config.providers = [
|
8
|
+
{ send_grid: { api_key: ENV.fetch("SENDGRID_API_KEY", nil) } },
|
9
|
+
{ mailgun: { domain: ENV.fetch("MAILGUN_DOMAIN", nil),
|
10
|
+
api_key: ENV.fetch("MAILGUN_API_KEY", nil) } }
|
11
|
+
# Add more providers as needed
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Set delivery method in environment config (e.g. config/environments/production.rb):
|
16
|
+
# config.action_mailer.delivery_method = :chain_mail
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :providers
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@providers = [] # e.g., [:onesignal, :brevo, :sendpulse]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# DSL for users
|
14
|
+
module ChainMail
|
15
|
+
class << self
|
16
|
+
attr_accessor :config
|
17
|
+
|
18
|
+
def configure
|
19
|
+
self.config ||= Configuration.new
|
20
|
+
yield(config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
class Delivery
|
5
|
+
def initialize(values = {})
|
6
|
+
# values can include any ActionMailer options like :to, :from, :subject
|
7
|
+
@settings = values
|
8
|
+
end
|
9
|
+
|
10
|
+
# Called by Rails ActionMailer
|
11
|
+
def deliver!(mail)
|
12
|
+
validate_mail!(mail)
|
13
|
+
results = try_providers(mail)
|
14
|
+
return unless results.none? { |r| r[:success] }
|
15
|
+
|
16
|
+
handle_delivery_errors(mail, results)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_mail!(mail)
|
22
|
+
required_fields = %i[to from subject body]
|
23
|
+
missing = required_fields.select do |field|
|
24
|
+
!mail.respond_to?(field) ||
|
25
|
+
(value = mail.send(field)).nil? ||
|
26
|
+
(value.respond_to?(:empty?) && value.empty?)
|
27
|
+
end
|
28
|
+
return if missing.empty?
|
29
|
+
|
30
|
+
raise "Mail object missing or blank: #{missing.join(', ')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def try_providers(mail)
|
34
|
+
providers = ChainMail.config.providers
|
35
|
+
validate_providers!(providers)
|
36
|
+
|
37
|
+
results = []
|
38
|
+
providers.each do |provider|
|
39
|
+
results.concat(try_single_provider(mail, provider))
|
40
|
+
return results if successful?(results)
|
41
|
+
end
|
42
|
+
results
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_providers!(providers)
|
46
|
+
raise "ChainMail: No providers configured" if providers.nil? || providers.empty?
|
47
|
+
return if providers.all? { |p| p.is_a?(Hash) && p.size == 1 }
|
48
|
+
|
49
|
+
raise "ChainMail: Provider config must be a hash with one key-value pair"
|
50
|
+
end
|
51
|
+
|
52
|
+
def successful?(results)
|
53
|
+
results.any? { |r| r[:success] }
|
54
|
+
end
|
55
|
+
|
56
|
+
def try_single_provider(mail, provider)
|
57
|
+
name, creds = provider.first
|
58
|
+
return invalid_credentials_result(name) unless creds.is_a?(Hash)
|
59
|
+
|
60
|
+
begin
|
61
|
+
result = provider_class(name).deliver(mail, creds)
|
62
|
+
return success_result(name, result) if result[:success]
|
63
|
+
|
64
|
+
error_result(name, result)
|
65
|
+
rescue StandardError => e
|
66
|
+
exception_result(name, e)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def invalid_credentials_result(name)
|
71
|
+
[{ provider: name, error: "Credentials must be a hash", response: nil }]
|
72
|
+
end
|
73
|
+
|
74
|
+
def success_result(name, result)
|
75
|
+
Rails.logger.info("[ChainMail] Email sent via #{name}")
|
76
|
+
[{ provider: name, success: true, error: nil, response: result[:response] }]
|
77
|
+
end
|
78
|
+
|
79
|
+
def error_result(name, result)
|
80
|
+
Rails.logger.error("[ChainMail] Provider #{name} failed: #{result[:error]}")
|
81
|
+
[{ provider: name, error: result[:error], response: result[:response] }]
|
82
|
+
end
|
83
|
+
|
84
|
+
def exception_result(name, exception)
|
85
|
+
Rails.logger.error("[ChainMail] Provider #{name} raised: #{exception.message}")
|
86
|
+
[{ provider: name, error: exception.message, response: nil }]
|
87
|
+
end
|
88
|
+
|
89
|
+
def handle_delivery_errors(mail, errors)
|
90
|
+
return if errors.empty?
|
91
|
+
|
92
|
+
error_messages = errors.map { |err| "#{err[:provider]}: #{err[:error]}" }.join("; ")
|
93
|
+
raise(
|
94
|
+
"ChainMail: All email providers failed for #{mail.to.join(', ')}. " \
|
95
|
+
"Errors: #{error_messages}"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Provider class resolution using ChainMail.provider_registry
|
100
|
+
def provider_class(name)
|
101
|
+
klass = ChainMail.provider_registry[name.to_sym]
|
102
|
+
raise "ChainMail: Unknown provider #{name}" unless klass
|
103
|
+
|
104
|
+
klass
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
module Providers
|
5
|
+
class Base
|
6
|
+
def self.deliver(mail, creds)
|
7
|
+
raise NotImplementedError, "Subclasses must implement deliver(mail, creds)"
|
8
|
+
end
|
9
|
+
|
10
|
+
# Helper for HTTP requests, error parsing, etc.
|
11
|
+
def self.post_json(url, headers, payload)
|
12
|
+
uri = URI(url)
|
13
|
+
req = Net::HTTP::Post.new(uri, headers)
|
14
|
+
req.body = payload.to_json
|
15
|
+
begin
|
16
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
17
|
+
if res.is_a?(Net::HTTPSuccess)
|
18
|
+
{ success: true, response: res, error: nil }
|
19
|
+
else
|
20
|
+
{ success: false, response: res, error: "API error: #{res.code} #{res.body}" }
|
21
|
+
end
|
22
|
+
rescue StandardError => e
|
23
|
+
{ success: false, response: nil, error: e.message }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
module Providers
|
5
|
+
class Brevo < Base
|
6
|
+
def self.deliver(mail, creds)
|
7
|
+
payload = {
|
8
|
+
sender: { email: mail.from.first },
|
9
|
+
to: mail.to.map { |t| { email: t } },
|
10
|
+
subject: mail.subject,
|
11
|
+
htmlContent: mail.body.decoded
|
12
|
+
}
|
13
|
+
headers = {
|
14
|
+
"api-key" => creds[:api_key],
|
15
|
+
"Content-Type" => "application/json"
|
16
|
+
}
|
17
|
+
|
18
|
+
headers.merge!("X-Sib-Sandbox" => "drop") if creds[:sandbox]
|
19
|
+
post_json("https://api.brevo.com/v3/smtp/email", headers, payload)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module ChainMail
|
8
|
+
module Providers
|
9
|
+
class Mailgun < Base
|
10
|
+
def self.deliver(mail, creds)
|
11
|
+
domain = creds[:domain]
|
12
|
+
api_key = creds[:api_key]
|
13
|
+
|
14
|
+
return error_result("MAILGUN_DOMAIN or MAILGUN_API_KEY not set") unless domain && api_key
|
15
|
+
|
16
|
+
send_mailgun_request(mail, domain, api_key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.send_mailgun_request(mail, domain, api_key)
|
20
|
+
req = build_mailgun_request(mail, domain, api_key)
|
21
|
+
perform_mailgun_request(req)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.build_mailgun_request(mail, domain, api_key)
|
25
|
+
uri = URI("https://api.mailgun.net/v3/#{domain}/messages")
|
26
|
+
req = Net::HTTP::Post.new(uri)
|
27
|
+
req.basic_auth("api", api_key)
|
28
|
+
req.set_form_data(
|
29
|
+
from: mail.from.first,
|
30
|
+
to: mail.to.join(","),
|
31
|
+
subject: mail.subject,
|
32
|
+
html: mail.body.decoded
|
33
|
+
)
|
34
|
+
req
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.perform_mailgun_request(req)
|
38
|
+
res = Net::HTTP.start(req.uri.hostname, req.uri.port, use_ssl: true) { |http| http.request(req) }
|
39
|
+
return success_result(res) if res.is_a?(Net::HTTPSuccess)
|
40
|
+
|
41
|
+
error_result("Mailgun API error: #{res.code} #{res.body}", res)
|
42
|
+
rescue StandardError => e
|
43
|
+
error_result(e.message)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.success_result(response)
|
47
|
+
{ success: true, response: response, error: nil }
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.error_result(error, response = nil)
|
51
|
+
{ success: false, response: response, error: error }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
module Providers
|
5
|
+
class OneSignal < Base
|
6
|
+
def self.deliver(mail, creds)
|
7
|
+
payload = {
|
8
|
+
include_email_tokens: mail.to,
|
9
|
+
subject: mail.subject,
|
10
|
+
body: mail.body.decoded,
|
11
|
+
from_email: mail.from.first
|
12
|
+
}
|
13
|
+
headers = {
|
14
|
+
"Authorization" => "Basic #{creds[:api_key]}",
|
15
|
+
"Content-Type" => "application/json"
|
16
|
+
}
|
17
|
+
post_json("https://onesignal.com/api/v1/notifications", headers, payload)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChainMail
|
4
|
+
module Providers
|
5
|
+
class Postmark < Base
|
6
|
+
def self.deliver(mail, creds)
|
7
|
+
payload = {
|
8
|
+
From: mail.from.first,
|
9
|
+
To: mail.to.join(","),
|
10
|
+
Subject: mail.subject,
|
11
|
+
HtmlBody: mail.body.decoded
|
12
|
+
}
|
13
|
+
headers = {
|
14
|
+
"X-Postmark-Server-Token" => creds[:api_key],
|
15
|
+
"Content-Type" => "application/json"
|
16
|
+
}
|
17
|
+
post_json("https://api.postmarkapp.com/email", headers, payload)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module ChainMail
|
7
|
+
module Providers
|
8
|
+
class SendGrid < Base
|
9
|
+
def self.deliver(mail, creds)
|
10
|
+
if creds[:api_key].nil? || creds[:api_key].to_s.strip.empty?
|
11
|
+
raise KeyError,
|
12
|
+
"Missing SendGrid API key"
|
13
|
+
end
|
14
|
+
|
15
|
+
payload = {
|
16
|
+
personalizations: [{ to: mail.to.map { |t| { email: t } } }],
|
17
|
+
from: { email: mail.from.first },
|
18
|
+
subject: mail.subject,
|
19
|
+
content: [{ type: "text/html", value: mail.body.decoded }]
|
20
|
+
}
|
21
|
+
headers = {
|
22
|
+
"Authorization" => "Bearer #{creds[:api_key]}",
|
23
|
+
"Content-Type" => "application/json"
|
24
|
+
}
|
25
|
+
post_json("https://api.sendgrid.com/v3/mail/send", headers, payload)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|