emailit 2.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +597 -0
  4. data/lib/emailit/api_resource.rb +22 -0
  5. data/lib/emailit/api_response.rb +18 -0
  6. data/lib/emailit/base_client.rb +115 -0
  7. data/lib/emailit/collection.rb +25 -0
  8. data/lib/emailit/delivery_method.rb +74 -0
  9. data/lib/emailit/emailit_client.rb +53 -0
  10. data/lib/emailit/emailit_object.rb +57 -0
  11. data/lib/emailit/errors.rb +36 -0
  12. data/lib/emailit/events/webhook_event.rb +215 -0
  13. data/lib/emailit/railtie.rb +13 -0
  14. data/lib/emailit/resources/api_key.rb +7 -0
  15. data/lib/emailit/resources/audience.rb +7 -0
  16. data/lib/emailit/resources/contact.rb +7 -0
  17. data/lib/emailit/resources/domain.rb +7 -0
  18. data/lib/emailit/resources/email.rb +7 -0
  19. data/lib/emailit/resources/email_verification.rb +7 -0
  20. data/lib/emailit/resources/email_verification_list.rb +7 -0
  21. data/lib/emailit/resources/event.rb +7 -0
  22. data/lib/emailit/resources/subscriber.rb +7 -0
  23. data/lib/emailit/resources/suppression.rb +7 -0
  24. data/lib/emailit/resources/template.rb +7 -0
  25. data/lib/emailit/resources/webhook.rb +7 -0
  26. data/lib/emailit/services/api_key_service.rb +27 -0
  27. data/lib/emailit/services/audience_service.rb +27 -0
  28. data/lib/emailit/services/base_service.rb +50 -0
  29. data/lib/emailit/services/contact_service.rb +27 -0
  30. data/lib/emailit/services/domain_service.rb +31 -0
  31. data/lib/emailit/services/email_service.rb +47 -0
  32. data/lib/emailit/services/email_verification_list_service.rb +27 -0
  33. data/lib/emailit/services/email_verification_service.rb +11 -0
  34. data/lib/emailit/services/event_service.rb +15 -0
  35. data/lib/emailit/services/subscriber_service.rb +27 -0
  36. data/lib/emailit/services/suppression_service.rb +27 -0
  37. data/lib/emailit/services/template_service.rb +31 -0
  38. data/lib/emailit/services/webhook_service.rb +27 -0
  39. data/lib/emailit/util.rb +35 -0
  40. data/lib/emailit/version.rb +5 -0
  41. data/lib/emailit/webhook_signature.rb +59 -0
  42. data/lib/emailit.rb +66 -0
  43. metadata +144 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Emailit
8
+ class BaseClient
9
+ DEFAULT_API_BASE = "https://api.emailit.com"
10
+
11
+ attr_reader :api_key, :api_base
12
+
13
+ def initialize(config)
14
+ config = { api_key: config } if config.is_a?(String)
15
+ config = symbolize_keys(config)
16
+
17
+ raise ArgumentError, "api_key is required" if config[:api_key].nil? || config[:api_key].empty?
18
+
19
+ @api_key = config[:api_key]
20
+ @api_base = (config[:api_base] || DEFAULT_API_BASE).chomp("/")
21
+ @timeout = config[:timeout] || 30
22
+ @open_timeout = config[:open_timeout] || 10
23
+ end
24
+
25
+ def request(method, path, params = nil)
26
+ uri = build_uri(path, method == :get ? params : nil)
27
+ req = build_request(method, uri, method == :get ? nil : params)
28
+
29
+ begin
30
+ http = Net::HTTP.new(uri.host, uri.port)
31
+ http.use_ssl = uri.scheme == "https"
32
+ http.read_timeout = @timeout
33
+ http.open_timeout = @open_timeout
34
+
35
+ response = http.request(req)
36
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Net::OpenTimeout => e
37
+ raise ConnectionError, "Could not connect to the Emailit API: #{e.message}"
38
+ rescue IOError, Net::ReadTimeout => e
39
+ raise ConnectionError, "Communication with Emailit API failed: #{e.message}"
40
+ end
41
+
42
+ status_code = response.code.to_i
43
+ headers = {}
44
+ response.each_header { |k, v| headers[k] = v }
45
+ body = response.body || ""
46
+
47
+ api_response = ApiResponse.new(status_code, headers, body)
48
+
49
+ handle_error_response(api_response) if status_code >= 400
50
+
51
+ api_response
52
+ end
53
+
54
+ private
55
+
56
+ def build_uri(path, query_params = nil)
57
+ uri = URI.parse("#{@api_base}#{path}")
58
+ if query_params && !query_params.empty?
59
+ uri.query = URI.encode_www_form(query_params)
60
+ end
61
+ uri
62
+ end
63
+
64
+ def build_request(method, uri, body_params)
65
+ klass = case method
66
+ when :get then Net::HTTP::Get
67
+ when :post then Net::HTTP::Post
68
+ when :patch then Net::HTTP::Patch
69
+ when :put then Net::HTTP::Put
70
+ when :delete then Net::HTTP::Delete
71
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
72
+ end
73
+
74
+ req = klass.new(uri)
75
+ req["Authorization"] = "Bearer #{@api_key}"
76
+ req["Content-Type"] = "application/json"
77
+ req["User-Agent"] = "emailit-ruby/#{VERSION}"
78
+ req.body = JSON.generate(body_params) if body_params
79
+ req
80
+ end
81
+
82
+ def handle_error_response(response)
83
+ message = extract_error_message(response)
84
+ raise ApiError.from_response(
85
+ message,
86
+ http_status: response.status_code,
87
+ http_body: response.body,
88
+ json_body: response.json,
89
+ http_headers: response.headers
90
+ )
91
+ end
92
+
93
+ def extract_error_message(response)
94
+ json = response.json
95
+ if json.is_a?(Hash)
96
+ error = json["error"]
97
+ if error.is_a?(String)
98
+ msg = error
99
+ msg += ": #{json["message"]}" if json["message"]
100
+ return msg
101
+ end
102
+
103
+ if error.is_a?(Hash) && error["message"]
104
+ return error["message"]
105
+ end
106
+ end
107
+
108
+ "API request failed with status #{response.status_code}"
109
+ end
110
+
111
+ def symbolize_keys(hash)
112
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Collection < EmailitObject
5
+ include Enumerable
6
+
7
+ def data
8
+ @values["data"] || []
9
+ end
10
+
11
+ def each(&block)
12
+ data.each(&block)
13
+ end
14
+
15
+ def count
16
+ data.length
17
+ end
18
+ alias size count
19
+ alias length count
20
+
21
+ def has_more?
22
+ !@values["next_page_url"].nil?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Emailit
6
+ class DeliveryMethod
7
+ attr_reader :settings
8
+
9
+ def initialize(settings = {})
10
+ @settings = settings
11
+ raise ArgumentError, "Emailit API key is required. Set config.action_mailer.emailit_settings = { api_key: 'your_key' }" unless @settings[:api_key]
12
+
13
+ @client = EmailitClient.new(@settings[:api_key])
14
+ end
15
+
16
+ def deliver!(mail)
17
+ params = build_params(mail)
18
+ @client.emails.send(params)
19
+ end
20
+
21
+ private
22
+
23
+ def build_params(mail)
24
+ params = {}
25
+
26
+ params["from"] = mail.from&.first || mail[:from]&.to_s
27
+ params["to"] = Array(mail.to)
28
+ params["subject"] = mail.subject if mail.subject
29
+
30
+ if mail.cc && !mail.cc.empty?
31
+ params["cc"] = Array(mail.cc)
32
+ end
33
+
34
+ if mail.bcc && !mail.bcc.empty?
35
+ params["bcc"] = Array(mail.bcc)
36
+ end
37
+
38
+ if mail.reply_to && !mail.reply_to.empty?
39
+ params["reply_to"] = Array(mail.reply_to).first
40
+ end
41
+
42
+ if mail.multipart?
43
+ mail.parts.each do |part|
44
+ case part.content_type
45
+ when /text\/html/
46
+ params["html"] = part.body.decoded
47
+ when /text\/plain/
48
+ params["text"] = part.body.decoded
49
+ end
50
+ end
51
+ elsif mail.content_type&.include?("text/html")
52
+ params["html"] = mail.body.decoded
53
+ else
54
+ params["text"] = mail.body.decoded
55
+ end
56
+
57
+ if mail.attachments.any?
58
+ params["attachments"] = mail.attachments.map do |attachment|
59
+ {
60
+ "filename" => attachment.filename,
61
+ "content" => Base64.strict_encode64(attachment.body.decoded),
62
+ "content_type" => attachment.content_type.split(";").first,
63
+ }
64
+ end
65
+ end
66
+
67
+ if mail.header["X-Emailit-Tags"]
68
+ params["tags"] = mail.header["X-Emailit-Tags"].to_s.split(",").map(&:strip)
69
+ end
70
+
71
+ params
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class EmailitClient < BaseClient
5
+ def emails
6
+ @emails ||= Services::EmailService.new(self)
7
+ end
8
+
9
+ def domains
10
+ @domains ||= Services::DomainService.new(self)
11
+ end
12
+
13
+ def api_keys
14
+ @api_keys ||= Services::ApiKeyService.new(self)
15
+ end
16
+
17
+ def audiences
18
+ @audiences ||= Services::AudienceService.new(self)
19
+ end
20
+
21
+ def subscribers
22
+ @subscribers ||= Services::SubscriberService.new(self)
23
+ end
24
+
25
+ def templates
26
+ @templates ||= Services::TemplateService.new(self)
27
+ end
28
+
29
+ def suppressions
30
+ @suppressions ||= Services::SuppressionService.new(self)
31
+ end
32
+
33
+ def email_verifications
34
+ @email_verifications ||= Services::EmailVerificationService.new(self)
35
+ end
36
+
37
+ def email_verification_lists
38
+ @email_verification_lists ||= Services::EmailVerificationListService.new(self)
39
+ end
40
+
41
+ def webhooks
42
+ @webhooks ||= Services::WebhookService.new(self)
43
+ end
44
+
45
+ def contacts
46
+ @contacts ||= Services::ContactService.new(self)
47
+ end
48
+
49
+ def events
50
+ @events ||= Services::EventService.new(self)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class EmailitObject
5
+ attr_reader :values
6
+ attr_accessor :last_response
7
+
8
+ def initialize(values = {})
9
+ @values = values || {}
10
+ end
11
+
12
+ def [](key)
13
+ @values[key.to_s]
14
+ end
15
+
16
+ def []=(key, value)
17
+ @values[key.to_s] = value
18
+ end
19
+
20
+ def key?(key)
21
+ @values.key?(key.to_s)
22
+ end
23
+
24
+ def to_hash
25
+ @values
26
+ end
27
+ alias to_h to_hash
28
+
29
+ def to_json(*args)
30
+ JSON.generate(@values, *args)
31
+ end
32
+
33
+ def to_s
34
+ JSON.pretty_generate(@values)
35
+ end
36
+
37
+ def respond_to_missing?(name, include_private = false)
38
+ @values.key?(name.to_s) || super
39
+ end
40
+
41
+ def method_missing(name, *args)
42
+ key = name.to_s
43
+ if key.end_with?("=")
44
+ @values[key.chomp("=")] = args.first
45
+ elsif @values.key?(key)
46
+ @values[key]
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def refresh_from(values)
53
+ @values = values
54
+ self
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Error < StandardError; end
5
+
6
+ class ConnectionError < Error; end
7
+
8
+ class ApiError < Error
9
+ attr_reader :http_status, :http_body, :json_body, :http_headers
10
+
11
+ def initialize(message = nil, http_status: 0, http_body: "", json_body: nil, http_headers: {})
12
+ @http_status = http_status
13
+ @http_body = http_body
14
+ @json_body = json_body
15
+ @http_headers = http_headers
16
+ super(message)
17
+ end
18
+
19
+ def self.from_response(message, http_status:, http_body:, json_body:, http_headers:)
20
+ klass = case http_status
21
+ when 401 then AuthenticationError
22
+ when 429 then RateLimitError
23
+ when 422 then UnprocessableEntityError
24
+ when 400, 404 then InvalidRequestError
25
+ else self
26
+ end
27
+
28
+ klass.new(message, http_status: http_status, http_body: http_body, json_body: json_body, http_headers: http_headers)
29
+ end
30
+ end
31
+
32
+ class AuthenticationError < ApiError; end
33
+ class InvalidRequestError < ApiError; end
34
+ class RateLimitError < ApiError; end
35
+ class UnprocessableEntityError < ApiError; end
36
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ module Events
5
+ class WebhookEvent < EmailitObject
6
+ EVENT_TYPE = ""
7
+
8
+ def self.construct_from(payload)
9
+ init_event_map unless @event_map_initialized
10
+
11
+ type = payload["type"]
12
+
13
+ if type && @event_map[type]
14
+ @event_map[type].new(payload)
15
+ else
16
+ new(payload)
17
+ end
18
+ end
19
+
20
+ def event_data
21
+ data = @values["data"]
22
+ return nil unless data
23
+
24
+ data["object"] || data
25
+ end
26
+
27
+ def self.init_event_map
28
+ @event_map = {
29
+ "email.accepted" => EmailAccepted,
30
+ "email.scheduled" => EmailScheduled,
31
+ "email.delivered" => EmailDelivered,
32
+ "email.bounced" => EmailBounced,
33
+ "email.attempted" => EmailAttempted,
34
+ "email.failed" => EmailFailed,
35
+ "email.rejected" => EmailRejected,
36
+ "email.suppressed" => EmailSuppressed,
37
+ "email.received" => EmailReceived,
38
+ "email.complained" => EmailComplained,
39
+ "email.clicked" => EmailClicked,
40
+ "email.loaded" => EmailLoaded,
41
+ "domain.created" => DomainCreated,
42
+ "domain.updated" => DomainUpdated,
43
+ "domain.deleted" => DomainDeleted,
44
+ "audience.created" => AudienceCreated,
45
+ "audience.updated" => AudienceUpdated,
46
+ "audience.deleted" => AudienceDeleted,
47
+ "subscriber.created" => SubscriberCreated,
48
+ "subscriber.updated" => SubscriberUpdated,
49
+ "subscriber.deleted" => SubscriberDeleted,
50
+ "contact.created" => ContactCreated,
51
+ "contact.updated" => ContactUpdated,
52
+ "contact.deleted" => ContactDeleted,
53
+ "template.created" => TemplateCreated,
54
+ "template.updated" => TemplateUpdated,
55
+ "template.deleted" => TemplateDeleted,
56
+ "suppression.created" => SuppressionCreated,
57
+ "suppression.updated" => SuppressionUpdated,
58
+ "suppression.deleted" => SuppressionDeleted,
59
+ "email_verification.created" => EmailVerificationCreated,
60
+ "email_verification.updated" => EmailVerificationUpdated,
61
+ "email_verification.deleted" => EmailVerificationDeleted,
62
+ "email_verification_list.created" => EmailVerificationListCreated,
63
+ "email_verification_list.updated" => EmailVerificationListUpdated,
64
+ "email_verification_list.deleted" => EmailVerificationListDeleted,
65
+ }
66
+ @event_map_initialized = true
67
+ end
68
+ private_class_method :init_event_map
69
+ end
70
+
71
+ class EmailAccepted < WebhookEvent
72
+ EVENT_TYPE = "email.accepted"
73
+ end
74
+
75
+ class EmailScheduled < WebhookEvent
76
+ EVENT_TYPE = "email.scheduled"
77
+ end
78
+
79
+ class EmailDelivered < WebhookEvent
80
+ EVENT_TYPE = "email.delivered"
81
+ end
82
+
83
+ class EmailBounced < WebhookEvent
84
+ EVENT_TYPE = "email.bounced"
85
+ end
86
+
87
+ class EmailAttempted < WebhookEvent
88
+ EVENT_TYPE = "email.attempted"
89
+ end
90
+
91
+ class EmailFailed < WebhookEvent
92
+ EVENT_TYPE = "email.failed"
93
+ end
94
+
95
+ class EmailRejected < WebhookEvent
96
+ EVENT_TYPE = "email.rejected"
97
+ end
98
+
99
+ class EmailSuppressed < WebhookEvent
100
+ EVENT_TYPE = "email.suppressed"
101
+ end
102
+
103
+ class EmailReceived < WebhookEvent
104
+ EVENT_TYPE = "email.received"
105
+ end
106
+
107
+ class EmailComplained < WebhookEvent
108
+ EVENT_TYPE = "email.complained"
109
+ end
110
+
111
+ class EmailClicked < WebhookEvent
112
+ EVENT_TYPE = "email.clicked"
113
+ end
114
+
115
+ class EmailLoaded < WebhookEvent
116
+ EVENT_TYPE = "email.loaded"
117
+ end
118
+
119
+ class DomainCreated < WebhookEvent
120
+ EVENT_TYPE = "domain.created"
121
+ end
122
+
123
+ class DomainUpdated < WebhookEvent
124
+ EVENT_TYPE = "domain.updated"
125
+ end
126
+
127
+ class DomainDeleted < WebhookEvent
128
+ EVENT_TYPE = "domain.deleted"
129
+ end
130
+
131
+ class AudienceCreated < WebhookEvent
132
+ EVENT_TYPE = "audience.created"
133
+ end
134
+
135
+ class AudienceUpdated < WebhookEvent
136
+ EVENT_TYPE = "audience.updated"
137
+ end
138
+
139
+ class AudienceDeleted < WebhookEvent
140
+ EVENT_TYPE = "audience.deleted"
141
+ end
142
+
143
+ class SubscriberCreated < WebhookEvent
144
+ EVENT_TYPE = "subscriber.created"
145
+ end
146
+
147
+ class SubscriberUpdated < WebhookEvent
148
+ EVENT_TYPE = "subscriber.updated"
149
+ end
150
+
151
+ class SubscriberDeleted < WebhookEvent
152
+ EVENT_TYPE = "subscriber.deleted"
153
+ end
154
+
155
+ class ContactCreated < WebhookEvent
156
+ EVENT_TYPE = "contact.created"
157
+ end
158
+
159
+ class ContactUpdated < WebhookEvent
160
+ EVENT_TYPE = "contact.updated"
161
+ end
162
+
163
+ class ContactDeleted < WebhookEvent
164
+ EVENT_TYPE = "contact.deleted"
165
+ end
166
+
167
+ class TemplateCreated < WebhookEvent
168
+ EVENT_TYPE = "template.created"
169
+ end
170
+
171
+ class TemplateUpdated < WebhookEvent
172
+ EVENT_TYPE = "template.updated"
173
+ end
174
+
175
+ class TemplateDeleted < WebhookEvent
176
+ EVENT_TYPE = "template.deleted"
177
+ end
178
+
179
+ class SuppressionCreated < WebhookEvent
180
+ EVENT_TYPE = "suppression.created"
181
+ end
182
+
183
+ class SuppressionUpdated < WebhookEvent
184
+ EVENT_TYPE = "suppression.updated"
185
+ end
186
+
187
+ class SuppressionDeleted < WebhookEvent
188
+ EVENT_TYPE = "suppression.deleted"
189
+ end
190
+
191
+ class EmailVerificationCreated < WebhookEvent
192
+ EVENT_TYPE = "email_verification.created"
193
+ end
194
+
195
+ class EmailVerificationUpdated < WebhookEvent
196
+ EVENT_TYPE = "email_verification.updated"
197
+ end
198
+
199
+ class EmailVerificationDeleted < WebhookEvent
200
+ EVENT_TYPE = "email_verification.deleted"
201
+ end
202
+
203
+ class EmailVerificationListCreated < WebhookEvent
204
+ EVENT_TYPE = "email_verification_list.created"
205
+ end
206
+
207
+ class EmailVerificationListUpdated < WebhookEvent
208
+ EVENT_TYPE = "email_verification_list.updated"
209
+ end
210
+
211
+ class EmailVerificationListDeleted < WebhookEvent
212
+ EVENT_TYPE = "email_verification_list.deleted"
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "delivery_method"
4
+
5
+ module Emailit
6
+ class Railtie < Rails::Railtie
7
+ initializer "emailit.add_delivery_method" do
8
+ ActiveSupport.on_load(:action_mailer) do
9
+ ActionMailer::Base.add_delivery_method(:emailit, Emailit::DeliveryMethod)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class ApiKey < ApiResource
5
+ OBJECT_NAME = "api_key"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Audience < ApiResource
5
+ OBJECT_NAME = "audience"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Contact < ApiResource
5
+ OBJECT_NAME = "contact"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Domain < ApiResource
5
+ OBJECT_NAME = "domain"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Email < ApiResource
5
+ OBJECT_NAME = "email"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class EmailVerification < ApiResource
5
+ OBJECT_NAME = "email_verification"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class EmailVerificationList < ApiResource
5
+ OBJECT_NAME = "email_verification_list"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Event < ApiResource
5
+ OBJECT_NAME = "event"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Subscriber < ApiResource
5
+ OBJECT_NAME = "subscriber"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Suppression < ApiResource
5
+ OBJECT_NAME = "suppression"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Template < ApiResource
5
+ OBJECT_NAME = "template"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailit
4
+ class Webhook < ApiResource
5
+ OBJECT_NAME = "webhook"
6
+ end
7
+ end