mailgazelle 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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE +21 -0
- data/README.md +265 -0
- data/docs/api.md +183 -0
- data/lib/mailgazelle/address.rb +33 -0
- data/lib/mailgazelle/attachment.rb +158 -0
- data/lib/mailgazelle/client.rb +61 -0
- data/lib/mailgazelle/configuration.rb +16 -0
- data/lib/mailgazelle/domain.rb +66 -0
- data/lib/mailgazelle/domains_resource.rb +32 -0
- data/lib/mailgazelle/email.rb +291 -0
- data/lib/mailgazelle/emails/attachment_meta.rb +50 -0
- data/lib/mailgazelle/emails/email_record.rb +91 -0
- data/lib/mailgazelle/emails/email_status.rb +19 -0
- data/lib/mailgazelle/emails/queued_email.rb +41 -0
- data/lib/mailgazelle/emails_resource.rb +35 -0
- data/lib/mailgazelle/error.rb +69 -0
- data/lib/mailgazelle/error_mapper.rb +57 -0
- data/lib/mailgazelle/http/http_client.rb +84 -0
- data/lib/mailgazelle/http/net_http_transport.rb +55 -0
- data/lib/mailgazelle/http/request.rb +10 -0
- data/lib/mailgazelle/http/response.rb +8 -0
- data/lib/mailgazelle/mailer.rb +212 -0
- data/lib/mailgazelle/railtie.rb +14 -0
- data/lib/mailgazelle/version.rb +5 -0
- data/lib/mailgazelle.rb +49 -0
- metadata +72 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailGazelle
|
|
4
|
+
# Base error for SDK failures and API error responses.
|
|
5
|
+
#
|
|
6
|
+
# Catch this class when any Mail Gazelle failure should take the same path.
|
|
7
|
+
# Use a subclass when recovery depends on the cause.
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
attr_reader :code, :http_status, :details
|
|
10
|
+
|
|
11
|
+
def initialize(message, code:, http_status: 0, details: {})
|
|
12
|
+
super(message)
|
|
13
|
+
@code = code
|
|
14
|
+
@http_status = http_status
|
|
15
|
+
@details = details
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when the mailer or default client is used without a token.
|
|
20
|
+
class ApiTokenMissing < Error
|
|
21
|
+
def initialize(message = "A Mail Gazelle API token is required. Set MAILGAZELLE_API_TOKEN or " \
|
|
22
|
+
"config.action_mailer.mailgazelle_settings[:api_token].")
|
|
23
|
+
super(message, code: "api_token_missing", http_status: 0)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# DNS, TLS, timeout, and other failures before an API response arrives.
|
|
28
|
+
class TransportError < Error
|
|
29
|
+
def initialize(message, code: "transport_error", http_status: 0, details: {})
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# An API error that does not map to a more specific class.
|
|
35
|
+
class ApiError < Error; end
|
|
36
|
+
|
|
37
|
+
# 401 unauthenticated, including a token for an archived product.
|
|
38
|
+
class AuthenticationError < Error; end
|
|
39
|
+
|
|
40
|
+
# 403 product_not_ready.
|
|
41
|
+
class ProductNotReadyError < Error; end
|
|
42
|
+
|
|
43
|
+
# 422 validation_error.
|
|
44
|
+
class ValidationError < Error; end
|
|
45
|
+
|
|
46
|
+
# 422 from_not_allowed.
|
|
47
|
+
class FromNotAllowedError < Error; end
|
|
48
|
+
|
|
49
|
+
# 422 recipient_suppressed. The address was stored as rejected and was not sent.
|
|
50
|
+
class RecipientSuppressedError < Error; end
|
|
51
|
+
|
|
52
|
+
# 422 attachment_invalid.
|
|
53
|
+
class AttachmentError < Error; end
|
|
54
|
+
|
|
55
|
+
# 422 attachment_too_large.
|
|
56
|
+
class AttachmentTooLargeError < Error; end
|
|
57
|
+
|
|
58
|
+
# 422 quota_exceeded.
|
|
59
|
+
class QuotaExceededError < Error; end
|
|
60
|
+
|
|
61
|
+
# 422 html_too_large.
|
|
62
|
+
class HtmlTooLargeError < Error; end
|
|
63
|
+
|
|
64
|
+
# 422 message_too_large.
|
|
65
|
+
class MessageTooLargeError < Error; end
|
|
66
|
+
|
|
67
|
+
# 429 rate_limited. The limit is 60 requests per minute per token.
|
|
68
|
+
class RateLimitError < Error; end
|
|
69
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mailgazelle/error"
|
|
5
|
+
|
|
6
|
+
module MailGazelle
|
|
7
|
+
# Maps a non-success HTTP response onto a typed {Error}.
|
|
8
|
+
module ErrorMapper
|
|
9
|
+
CLASSES = {
|
|
10
|
+
"unauthenticated" => AuthenticationError,
|
|
11
|
+
"product_not_ready" => ProductNotReadyError,
|
|
12
|
+
"validation_error" => ValidationError,
|
|
13
|
+
"from_not_allowed" => FromNotAllowedError,
|
|
14
|
+
"recipient_suppressed" => RecipientSuppressedError,
|
|
15
|
+
"attachment_invalid" => AttachmentError,
|
|
16
|
+
"attachment_too_large" => AttachmentTooLargeError,
|
|
17
|
+
"quota_exceeded" => QuotaExceededError,
|
|
18
|
+
"html_too_large" => HtmlTooLargeError,
|
|
19
|
+
"message_too_large" => MessageTooLargeError,
|
|
20
|
+
"rate_limited" => RateLimitError
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def from_response(response)
|
|
26
|
+
payload = decode(response.body)
|
|
27
|
+
code = string_or_nil(payload["code"]) || code_from_status(response.status_code)
|
|
28
|
+
message = string_or_nil(payload["message"]) || "Unexpected Mail Gazelle API error."
|
|
29
|
+
details = payload["details"].is_a?(Hash) ? payload["details"] : {}
|
|
30
|
+
error_class = CLASSES.fetch(code, ApiError)
|
|
31
|
+
error_class.new(message, code: code, http_status: response.status_code, details: details)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def decode(body)
|
|
35
|
+
return {} if body.nil? || body.empty?
|
|
36
|
+
|
|
37
|
+
decoded = JSON.parse(body)
|
|
38
|
+
decoded.is_a?(Hash) ? decoded : {}
|
|
39
|
+
rescue JSON::ParserError
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def code_from_status(status)
|
|
44
|
+
case status
|
|
45
|
+
when 401 then "unauthenticated"
|
|
46
|
+
when 403 then "product_not_ready"
|
|
47
|
+
when 422 then "validation_error"
|
|
48
|
+
when 429 then "rate_limited"
|
|
49
|
+
else "unknown_error"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def string_or_nil(value)
|
|
54
|
+
value.is_a?(String) && !value.empty? ? value : nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mailgazelle/error"
|
|
5
|
+
require "mailgazelle/error_mapper"
|
|
6
|
+
require "mailgazelle/http/request"
|
|
7
|
+
|
|
8
|
+
module MailGazelle
|
|
9
|
+
module Http
|
|
10
|
+
# Authenticates requests, encodes JSON, and maps API errors to exceptions.
|
|
11
|
+
class HttpClient
|
|
12
|
+
def initialize(transport:, base_url:, default_headers:, timeout:, connect_timeout:)
|
|
13
|
+
@transport = transport
|
|
14
|
+
@base_url = base_url
|
|
15
|
+
@default_headers = default_headers
|
|
16
|
+
@timeout = timeout
|
|
17
|
+
@connect_timeout = connect_timeout
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Send an authenticated JSON request and return the decoded object or list.
|
|
21
|
+
def request(method, path, json: nil)
|
|
22
|
+
headers = @default_headers.dup
|
|
23
|
+
body = nil
|
|
24
|
+
|
|
25
|
+
unless json.nil?
|
|
26
|
+
body = encode(json)
|
|
27
|
+
headers["Content-Type"] = "application/json"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response = @transport.perform(
|
|
31
|
+
Request.new(
|
|
32
|
+
http_method: method.to_s.upcase,
|
|
33
|
+
url: url(path),
|
|
34
|
+
headers: headers,
|
|
35
|
+
body: body,
|
|
36
|
+
timeout: @timeout,
|
|
37
|
+
connect_timeout: @connect_timeout
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
raise ErrorMapper.from_response(response) unless response.status_code.between?(200, 299)
|
|
42
|
+
|
|
43
|
+
decode_success(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def url(path)
|
|
47
|
+
"#{@base_url}/#{path.to_s.sub(%r{\A/}, '')}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def encode(json)
|
|
53
|
+
JSON.generate(json)
|
|
54
|
+
rescue JSON::GeneratorError => e
|
|
55
|
+
raise ApiError.new(
|
|
56
|
+
"Failed to encode the request body as JSON.",
|
|
57
|
+
code: "invalid_request",
|
|
58
|
+
http_status: 0
|
|
59
|
+
), cause: e
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def decode_success(response)
|
|
63
|
+
return {} if response.body.nil? || response.body.empty?
|
|
64
|
+
|
|
65
|
+
decoded = JSON.parse(response.body)
|
|
66
|
+
unless decoded.is_a?(Hash) || decoded.is_a?(Array)
|
|
67
|
+
raise ApiError.new(
|
|
68
|
+
"API returned a non-object JSON payload.",
|
|
69
|
+
code: "invalid_response",
|
|
70
|
+
http_status: response.status_code
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
decoded
|
|
75
|
+
rescue JSON::ParserError => e
|
|
76
|
+
raise ApiError.new(
|
|
77
|
+
"Failed to decode the API response.",
|
|
78
|
+
code: "invalid_response",
|
|
79
|
+
http_status: response.status_code
|
|
80
|
+
), cause: e
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "mailgazelle/error"
|
|
7
|
+
require "mailgazelle/http/request"
|
|
8
|
+
require "mailgazelle/http/response"
|
|
9
|
+
|
|
10
|
+
module MailGazelle
|
|
11
|
+
module Http
|
|
12
|
+
# Default transport backed by +Net::HTTP+. Inject another object with +perform+
|
|
13
|
+
# when an application wants a different HTTP stack or a test double.
|
|
14
|
+
class NetHttpTransport
|
|
15
|
+
METHODS = {
|
|
16
|
+
"GET" => Net::HTTP::Get,
|
|
17
|
+
"POST" => Net::HTTP::Post,
|
|
18
|
+
"PATCH" => Net::HTTP::Patch,
|
|
19
|
+
"PUT" => Net::HTTP::Put,
|
|
20
|
+
"DELETE" => Net::HTTP::Delete
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def perform(request)
|
|
24
|
+
uri = URI.parse(request.url)
|
|
25
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
26
|
+
http.use_ssl = uri.scheme == "https"
|
|
27
|
+
http.open_timeout = request.connect_timeout
|
|
28
|
+
http.read_timeout = request.timeout
|
|
29
|
+
http.write_timeout = request.timeout if http.respond_to?(:write_timeout=)
|
|
30
|
+
|
|
31
|
+
response = http.request(build_request(uri, request))
|
|
32
|
+
headers = {}
|
|
33
|
+
response.each_header { |key, value| headers[key.downcase] = value }
|
|
34
|
+
|
|
35
|
+
Response.new(status_code: response.code.to_i, body: response.body.to_s, headers: headers)
|
|
36
|
+
rescue Timeout::Error, IOError, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
|
|
37
|
+
message = e.message.to_s
|
|
38
|
+
message = "The HTTP request failed." if message.empty?
|
|
39
|
+
raise TransportError.new(message, details: { "error_class" => e.class.name }), cause: e
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_request(uri, request)
|
|
45
|
+
klass = METHODS[request.http_method.to_s.upcase]
|
|
46
|
+
raise TransportError, "Unsupported HTTP method #{request.http_method.inspect}." if klass.nil?
|
|
47
|
+
|
|
48
|
+
http_request = klass.new(uri.request_uri)
|
|
49
|
+
request.headers.each { |name, value| http_request[name] = value }
|
|
50
|
+
http_request.body = request.body unless request.body.nil?
|
|
51
|
+
http_request
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailGazelle
|
|
4
|
+
module Http
|
|
5
|
+
# Outgoing HTTP request passed to a transport.
|
|
6
|
+
#
|
|
7
|
+
# A transport is any object that implements +perform+ and returns a {Response}.
|
|
8
|
+
Request = Data.define(:http_method, :url, :headers, :body, :timeout, :connect_timeout)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mail"
|
|
4
|
+
require "mailgazelle/attachment"
|
|
5
|
+
require "mailgazelle/client"
|
|
6
|
+
require "mailgazelle/email"
|
|
7
|
+
require "mailgazelle/error"
|
|
8
|
+
|
|
9
|
+
module MailGazelle
|
|
10
|
+
# ActionMailer delivery method. Registered as +:mailgazelle+ by {Railtie}.
|
|
11
|
+
class Mailer
|
|
12
|
+
SKIPPED_HEADERS = %w[
|
|
13
|
+
bcc
|
|
14
|
+
cc
|
|
15
|
+
content-transfer-encoding
|
|
16
|
+
content-type
|
|
17
|
+
date
|
|
18
|
+
from
|
|
19
|
+
idempotency-key
|
|
20
|
+
idempotency_key
|
|
21
|
+
mailgazelle-idempotency-key
|
|
22
|
+
mime-version
|
|
23
|
+
reply-to
|
|
24
|
+
return-path
|
|
25
|
+
sender
|
|
26
|
+
subject
|
|
27
|
+
tags
|
|
28
|
+
to
|
|
29
|
+
x-mailgazelle-message-id
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
IDEMPOTENCY_HEADERS = %w[
|
|
33
|
+
idempotency-key
|
|
34
|
+
idempotency_key
|
|
35
|
+
mailgazelle-idempotency-key
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :settings
|
|
39
|
+
|
|
40
|
+
def initialize(settings = nil)
|
|
41
|
+
@config = settings.is_a?(Hash) ? settings : {}
|
|
42
|
+
@settings = @config.merge(return_response: true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def deliver!(mail)
|
|
46
|
+
queued = client.emails.send(build_email(mail))
|
|
47
|
+
remember_id(mail, queued.id)
|
|
48
|
+
queued
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def client
|
|
54
|
+
@client ||= build_client
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_client
|
|
58
|
+
token = first_present(setting(:api_token), MailGazelle.config.api_token)
|
|
59
|
+
raise ApiTokenMissing if token.nil? || token.to_s.strip.empty?
|
|
60
|
+
|
|
61
|
+
Client.new(
|
|
62
|
+
api_token: token,
|
|
63
|
+
base_url: first_present(setting(:api_base), setting(:base_url), MailGazelle.config.api_base),
|
|
64
|
+
transport: setting(:transport),
|
|
65
|
+
timeout: first_present(setting(:timeout), MailGazelle.config.timeout) || 30,
|
|
66
|
+
connect_timeout: first_present(setting(:connect_timeout), MailGazelle.config.connect_timeout) || 10,
|
|
67
|
+
user_agent_suffix: user_agent_suffix
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def user_agent_suffix
|
|
72
|
+
extra = first_present(setting(:user_agent_suffix), MailGazelle.config.user_agent_suffix)
|
|
73
|
+
["rails", extra].compact.map { |value| value.to_s.strip }.reject(&:empty?).join(" ")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def setting(key)
|
|
77
|
+
return @config[key] if @config.key?(key)
|
|
78
|
+
|
|
79
|
+
@config[key.to_s] if @config.key?(key.to_s)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def first_present(*values)
|
|
83
|
+
values.find { |value| !value.nil? && !(value.is_a?(String) && value.strip.empty?) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_email(message)
|
|
87
|
+
recipients = address_pairs(message[:to])
|
|
88
|
+
if recipients.empty?
|
|
89
|
+
raise ValidationError.new(
|
|
90
|
+
"A Mail Gazelle message requires at least one To address.",
|
|
91
|
+
code: "validation_error",
|
|
92
|
+
http_status: 422
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
email = Email.to(recipients.first[0], recipients.first[1])
|
|
97
|
+
recipients.drop(1).each { |address, name| email = email.add_to(address, name) }
|
|
98
|
+
address_pairs(message[:cc]).each { |address, name| email = email.cc(address, name) }
|
|
99
|
+
address_pairs(message[:bcc]).each { |address, name| email = email.bcc(address, name) }
|
|
100
|
+
email = email.subject(message.subject.to_s)
|
|
101
|
+
email = apply_body(email, message)
|
|
102
|
+
|
|
103
|
+
from = address_pairs(message[:from]).first
|
|
104
|
+
email = email.from(from[0], from[1]) if from
|
|
105
|
+
address_pairs(message[:reply_to]).each { |address, name| email = email.reply_to(address, name) }
|
|
106
|
+
email = apply_headers(email, message)
|
|
107
|
+
apply_attachments(email, message)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_body(email, message)
|
|
111
|
+
if message.multipart?
|
|
112
|
+
text = part_body(message.text_part)
|
|
113
|
+
html = part_body(message.html_part)
|
|
114
|
+
email = email.text(text) if text
|
|
115
|
+
email = email.html(html) if html
|
|
116
|
+
return email
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
body = message.body.decoded.to_s
|
|
120
|
+
return email if body.empty?
|
|
121
|
+
|
|
122
|
+
if message.mime_type == "text/html"
|
|
123
|
+
email.html(body)
|
|
124
|
+
else
|
|
125
|
+
email.text(body)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def part_body(part)
|
|
130
|
+
return nil if part.nil?
|
|
131
|
+
|
|
132
|
+
body = part.decoded.to_s
|
|
133
|
+
body.empty? ? nil : body
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def apply_headers(email, message)
|
|
137
|
+
tags = nil
|
|
138
|
+
idempotency = nil
|
|
139
|
+
headers = {}
|
|
140
|
+
|
|
141
|
+
message.header_fields.each do |field|
|
|
142
|
+
name = field.name.to_s
|
|
143
|
+
lowered = name.downcase
|
|
144
|
+
|
|
145
|
+
if lowered == "tags"
|
|
146
|
+
tags = hash_value(field)
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if IDEMPOTENCY_HEADERS.include?(lowered)
|
|
151
|
+
idempotency = field.value.to_s
|
|
152
|
+
next
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
next if SKIPPED_HEADERS.include?(lowered)
|
|
156
|
+
|
|
157
|
+
value = field.value
|
|
158
|
+
next if value.nil?
|
|
159
|
+
|
|
160
|
+
headers[name] = value.to_s
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
headers.each { |name, value| email = email.header(name, value) }
|
|
164
|
+
tags&.each { |key, value| email = email.tag(key.to_s, value.to_s) }
|
|
165
|
+
email = email.idempotency_key(idempotency.strip) if idempotency.is_a?(String) && !idempotency.strip.empty?
|
|
166
|
+
email
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def hash_value(field)
|
|
170
|
+
unparsed = field.unparsed_value if field.respond_to?(:unparsed_value)
|
|
171
|
+
return unparsed if unparsed.is_a?(Hash)
|
|
172
|
+
return field.value if field.value.is_a?(Hash)
|
|
173
|
+
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def apply_attachments(email, message)
|
|
178
|
+
message.attachments.each do |part|
|
|
179
|
+
filename = part.filename.to_s
|
|
180
|
+
filename = "attachment" if filename.empty?
|
|
181
|
+
email = email.attach(
|
|
182
|
+
Attachment.from_contents(
|
|
183
|
+
filename,
|
|
184
|
+
part.body.decoded.to_s,
|
|
185
|
+
content_type: part.mime_type,
|
|
186
|
+
content_id: part.inline? ? part.cid : nil
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
email
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def address_pairs(header)
|
|
194
|
+
return [] if header.nil? || !header.respond_to?(:addrs)
|
|
195
|
+
|
|
196
|
+
header.addrs.filter_map do |address|
|
|
197
|
+
email = address.address.to_s.strip
|
|
198
|
+
next if email.empty?
|
|
199
|
+
|
|
200
|
+
name = address.display_name.to_s.strip
|
|
201
|
+
[email, name.empty? ? nil : name]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def remember_id(message, id)
|
|
206
|
+
return if id.nil? || id.empty?
|
|
207
|
+
|
|
208
|
+
message.header["X-MailGazelle-Message-Id"] = id
|
|
209
|
+
message.message_id = id
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mailgazelle/mailer"
|
|
4
|
+
|
|
5
|
+
module MailGazelle
|
|
6
|
+
# Registers the +:mailgazelle+ ActionMailer delivery method when Rails boots.
|
|
7
|
+
class Railtie < Rails::Railtie
|
|
8
|
+
initializer "mailgazelle.action_mailer" do
|
|
9
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
10
|
+
add_delivery_method :mailgazelle, MailGazelle::Mailer
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/mailgazelle.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mailgazelle/version"
|
|
4
|
+
require "mailgazelle/error"
|
|
5
|
+
require "mailgazelle/configuration"
|
|
6
|
+
require "mailgazelle/address"
|
|
7
|
+
require "mailgazelle/attachment"
|
|
8
|
+
require "mailgazelle/email"
|
|
9
|
+
require "mailgazelle/client"
|
|
10
|
+
|
|
11
|
+
module MailGazelle
|
|
12
|
+
class << self
|
|
13
|
+
def configure
|
|
14
|
+
yield config
|
|
15
|
+
@client = nil
|
|
16
|
+
config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def config
|
|
20
|
+
@config ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def client
|
|
24
|
+
@client ||= default_client
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset!
|
|
28
|
+
@config = Configuration.new
|
|
29
|
+
@client = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def default_client
|
|
35
|
+
token = config.api_token
|
|
36
|
+
raise ApiTokenMissing if token.nil? || token.to_s.strip.empty?
|
|
37
|
+
|
|
38
|
+
Client.new(
|
|
39
|
+
api_token: token,
|
|
40
|
+
base_url: config.api_base,
|
|
41
|
+
timeout: config.timeout || 30,
|
|
42
|
+
connect_timeout: config.connect_timeout || 10,
|
|
43
|
+
user_agent_suffix: config.user_agent_suffix
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
require "mailgazelle/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mailgazelle
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Srdjan Marjanovic
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Send transactional email with the Mail Gazelle API from Ruby, and from
|
|
13
|
+
Rails via ActionMailer.
|
|
14
|
+
email:
|
|
15
|
+
- srdjan@foobar.rs
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- docs/api.md
|
|
24
|
+
- lib/mailgazelle.rb
|
|
25
|
+
- lib/mailgazelle/address.rb
|
|
26
|
+
- lib/mailgazelle/attachment.rb
|
|
27
|
+
- lib/mailgazelle/client.rb
|
|
28
|
+
- lib/mailgazelle/configuration.rb
|
|
29
|
+
- lib/mailgazelle/domain.rb
|
|
30
|
+
- lib/mailgazelle/domains_resource.rb
|
|
31
|
+
- lib/mailgazelle/email.rb
|
|
32
|
+
- lib/mailgazelle/emails/attachment_meta.rb
|
|
33
|
+
- lib/mailgazelle/emails/email_record.rb
|
|
34
|
+
- lib/mailgazelle/emails/email_status.rb
|
|
35
|
+
- lib/mailgazelle/emails/queued_email.rb
|
|
36
|
+
- lib/mailgazelle/emails_resource.rb
|
|
37
|
+
- lib/mailgazelle/error.rb
|
|
38
|
+
- lib/mailgazelle/error_mapper.rb
|
|
39
|
+
- lib/mailgazelle/http/http_client.rb
|
|
40
|
+
- lib/mailgazelle/http/net_http_transport.rb
|
|
41
|
+
- lib/mailgazelle/http/request.rb
|
|
42
|
+
- lib/mailgazelle/http/response.rb
|
|
43
|
+
- lib/mailgazelle/mailer.rb
|
|
44
|
+
- lib/mailgazelle/railtie.rb
|
|
45
|
+
- lib/mailgazelle/version.rb
|
|
46
|
+
homepage: https://mailgazelle.com
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://mailgazelle.com
|
|
51
|
+
source_code_uri: https://github.com/mailgazelle/ruby-sdk
|
|
52
|
+
changelog_uri: https://github.com/mailgazelle/ruby-sdk/blob/main/CHANGELOG.md
|
|
53
|
+
allowed_push_host: https://rubygems.org
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '3.2'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 4.0.4
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Ruby and Rails SDK for sending transactional email through Mail Gazelle.
|
|
72
|
+
test_files: []
|