emailfuse 0.1.4 → 0.2.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 +4 -4
- data/CHANGELOG.md +131 -0
- data/README.md +90 -9
- data/lib/email_fuse/batch.rb +36 -0
- data/lib/email_fuse/client.rb +19 -0
- data/lib/email_fuse/emails/attachments.rb +67 -0
- data/lib/email_fuse/emails/receiving/attachments.rb +69 -0
- data/lib/email_fuse/emails/receiving.rb +43 -0
- data/lib/email_fuse/emails.rb +55 -0
- data/lib/email_fuse/errors.rb +52 -0
- data/lib/email_fuse/mailer.rb +257 -0
- data/lib/email_fuse/pagination_helper.rb +30 -0
- data/lib/email_fuse/railtie.rb +14 -0
- data/lib/email_fuse/request.rb +121 -0
- data/lib/email_fuse/response.rb +141 -0
- data/lib/email_fuse/version.rb +5 -0
- data/lib/email_fuse/webhooks.rb +239 -0
- data/lib/email_fuse.rb +43 -0
- data/lib/emailfuse.rb +2 -31
- metadata +31 -45
- data/Rakefile +0 -10
- data/lib/emailfuse/client.rb +0 -69
- data/lib/emailfuse/collection.rb +0 -27
- data/lib/emailfuse/configuration.rb +0 -10
- data/lib/emailfuse/deliverer.rb +0 -72
- data/lib/emailfuse/error.rb +0 -4
- data/lib/emailfuse/models/email.rb +0 -21
- data/lib/emailfuse/object.rb +0 -19
- data/lib/emailfuse/railtie.rb +0 -12
- data/lib/emailfuse/version.rb +0 -5
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module EmailFuse
|
|
7
|
+
# The Webhooks module provides methods for managing webhooks via the EmailFuse API.
|
|
8
|
+
# Webhooks allow you to receive real-time notifications about email events.
|
|
9
|
+
#
|
|
10
|
+
# Default tolerance for timestamp validation (5 minutes)
|
|
11
|
+
WEBHOOK_TOLERANCE_SECONDS = 300
|
|
12
|
+
#
|
|
13
|
+
# @example Create a webhook
|
|
14
|
+
# EmailFuse::Webhooks.create(
|
|
15
|
+
# endpoint: "https://webhook.example.com/handler",
|
|
16
|
+
# events: ["email.sent", "email.delivered", "email.bounced"]
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example List all webhooks
|
|
20
|
+
# EmailFuse::Webhooks.list
|
|
21
|
+
#
|
|
22
|
+
# @example Retrieve a specific webhook
|
|
23
|
+
# EmailFuse::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
|
|
24
|
+
#
|
|
25
|
+
# @example Update a webhook
|
|
26
|
+
# EmailFuse::Webhooks.update(
|
|
27
|
+
# webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
|
|
28
|
+
# endpoint: "https://new-webhook.example.com/handler",
|
|
29
|
+
# events: ["email.sent", "email.delivered"],
|
|
30
|
+
# status: "enabled"
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# @example Delete a webhook
|
|
34
|
+
# EmailFuse::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
|
|
35
|
+
module Webhooks
|
|
36
|
+
class << self
|
|
37
|
+
# Create a new webhook to receive real-time notifications about email events
|
|
38
|
+
#
|
|
39
|
+
# @param params [Hash] The webhook parameters
|
|
40
|
+
# @option params [String] :endpoint The URL where webhook events will be sent (required)
|
|
41
|
+
# @option params [Array<String>] :events Array of event types to subscribe to (required)
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] The webhook object containing id, object type, and signing_secret
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# EmailFuse::Webhooks.create(
|
|
47
|
+
# endpoint: "https://webhook.example.com/handler",
|
|
48
|
+
# events: ["email.sent", "email.delivered", "email.bounced"]
|
|
49
|
+
# )
|
|
50
|
+
def create(params = {})
|
|
51
|
+
path = "webhooks"
|
|
52
|
+
EmailFuse::Request.new(path, params, "post").perform
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retrieve a list of webhooks for the authenticated user
|
|
56
|
+
#
|
|
57
|
+
# @param params [Hash] The pagination parameters
|
|
58
|
+
# @option params [Integer] :limit Number of webhooks to retrieve (max: 100, min: 1)
|
|
59
|
+
# @option params [String] :after The ID after which to retrieve more webhooks (for pagination)
|
|
60
|
+
# @option params [String] :before The ID before which to retrieve more webhooks (for pagination)
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash] A paginated list of webhook objects
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# EmailFuse::Webhooks.list
|
|
66
|
+
#
|
|
67
|
+
# @example With pagination
|
|
68
|
+
# EmailFuse::Webhooks.list(limit: 20, after: "4dd369bc-aa82-4ff3-97de-514ae3000ee0")
|
|
69
|
+
def list(params = {})
|
|
70
|
+
path = EmailFuse::PaginationHelper.build_paginated_path("webhooks", params)
|
|
71
|
+
EmailFuse::Request.new(path, {}, "get").perform
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Retrieve a single webhook for the authenticated user
|
|
75
|
+
#
|
|
76
|
+
# @param webhook_id [String] The webhook ID
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash] The webhook object with full details
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# EmailFuse::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
|
|
82
|
+
def get(webhook_id = "")
|
|
83
|
+
path = "webhooks/#{webhook_id}"
|
|
84
|
+
EmailFuse::Request.new(path, {}, "get").perform
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Update an existing webhook configuration
|
|
88
|
+
#
|
|
89
|
+
# @param params [Hash] The webhook update parameters
|
|
90
|
+
# @option params [String] :webhook_id The webhook ID (required)
|
|
91
|
+
# @option params [String] :endpoint The URL where webhook events will be sent
|
|
92
|
+
# @option params [Array<String>] :events Array of event types to subscribe to
|
|
93
|
+
# @option params [String] :status The webhook status ("enabled" or "disabled")
|
|
94
|
+
#
|
|
95
|
+
# @return [Hash] The updated webhook object
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# EmailFuse::Webhooks.update(
|
|
99
|
+
# webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
|
|
100
|
+
# endpoint: "https://new-webhook.example.com/handler",
|
|
101
|
+
# events: ["email.sent", "email.delivered"],
|
|
102
|
+
# status: "enabled"
|
|
103
|
+
# )
|
|
104
|
+
def update(params = {})
|
|
105
|
+
webhook_id = params.delete(:webhook_id)
|
|
106
|
+
path = "webhooks/#{webhook_id}"
|
|
107
|
+
EmailFuse::Request.new(path, params, "patch").perform
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Remove an existing webhook
|
|
111
|
+
#
|
|
112
|
+
# @param webhook_id [String] The webhook ID
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash] Confirmation object with id, object type, and deleted status
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# EmailFuse::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
|
|
118
|
+
def remove(webhook_id = "")
|
|
119
|
+
path = "webhooks/#{webhook_id}"
|
|
120
|
+
EmailFuse::Request.new(path, {}, "delete").perform
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Verify a webhook payload using HMAC-SHA256 signature validation
|
|
124
|
+
# This validates that the webhook request came from EmailFuse and hasn't been tampered with
|
|
125
|
+
#
|
|
126
|
+
# @param params [Hash] The webhook verification parameters
|
|
127
|
+
# @option params [String] :payload The raw webhook payload body (required)
|
|
128
|
+
# @option params [Hash] :headers The webhook headers containing svix-id, svix-timestamp,
|
|
129
|
+
# and svix-signature (required)
|
|
130
|
+
# @option params [String] :webhook_secret The signing secret from webhook creation (required)
|
|
131
|
+
#
|
|
132
|
+
# @return [Boolean] true if verification succeeds
|
|
133
|
+
# @raise [StandardError] If verification fails or required parameters are missing
|
|
134
|
+
#
|
|
135
|
+
# @example
|
|
136
|
+
# EmailFuse::Webhooks.verify(
|
|
137
|
+
# payload: request.body.read,
|
|
138
|
+
# headers: {
|
|
139
|
+
# svix_id: "id_1234567890abcdefghijklmnopqrstuvwxyz",
|
|
140
|
+
# svix_timestamp: "1616161616",
|
|
141
|
+
# svix_signature: "v1,signature_here"
|
|
142
|
+
# },
|
|
143
|
+
# webhook_secret: "whsec_1234567890abcdez"
|
|
144
|
+
# )
|
|
145
|
+
def verify(params = {})
|
|
146
|
+
payload = params[:payload]
|
|
147
|
+
headers = params[:headers] || {}
|
|
148
|
+
webhook_secret = params[:webhook_secret]
|
|
149
|
+
|
|
150
|
+
validate_required_params(payload, headers, webhook_secret)
|
|
151
|
+
validate_timestamp(headers[:svix_timestamp])
|
|
152
|
+
|
|
153
|
+
signed_content = "#{headers[:svix_id]}.#{headers[:svix_timestamp]}.#{payload}"
|
|
154
|
+
decoded_secret = decode_secret(webhook_secret)
|
|
155
|
+
expected_signature = generate_signature(decoded_secret, signed_content)
|
|
156
|
+
|
|
157
|
+
verify_signature(headers[:svix_signature], expected_signature)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Validate required parameters
|
|
163
|
+
def validate_required_params(payload, headers, webhook_secret)
|
|
164
|
+
validate_payload(payload)
|
|
165
|
+
validate_webhook_secret(webhook_secret)
|
|
166
|
+
validate_headers(headers)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Validate payload is present
|
|
170
|
+
def validate_payload(payload)
|
|
171
|
+
raise "payload cannot be empty" if payload.nil? || payload.empty?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Validate webhook secret is present
|
|
175
|
+
def validate_webhook_secret(webhook_secret)
|
|
176
|
+
raise "webhook_secret cannot be empty" if webhook_secret.nil? || webhook_secret.empty?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Validate required headers are present
|
|
180
|
+
def validate_headers(headers)
|
|
181
|
+
raise "svix-id header is required" if headers[:svix_id].nil? || headers[:svix_id].empty?
|
|
182
|
+
raise "svix-timestamp header is required" if headers[:svix_timestamp].nil? || headers[:svix_timestamp].empty?
|
|
183
|
+
raise "svix-signature header is required" if headers[:svix_signature].nil? || headers[:svix_signature].empty?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Validate timestamp to prevent replay attacks
|
|
187
|
+
def validate_timestamp(timestamp_header)
|
|
188
|
+
timestamp = timestamp_header.to_i
|
|
189
|
+
now = Time.now.to_i
|
|
190
|
+
diff = now - timestamp
|
|
191
|
+
|
|
192
|
+
return unless diff > WEBHOOK_TOLERANCE_SECONDS || diff < -WEBHOOK_TOLERANCE_SECONDS
|
|
193
|
+
|
|
194
|
+
raise "Timestamp outside tolerance window: difference of #{diff} seconds"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Decode the signing secret (strip whsec_ prefix and base64 decode)
|
|
198
|
+
def decode_secret(webhook_secret)
|
|
199
|
+
secret = webhook_secret.sub(/^whsec_/, "")
|
|
200
|
+
Base64.strict_decode64(secret)
|
|
201
|
+
rescue ArgumentError => e
|
|
202
|
+
raise "Failed to decode webhook secret: #{e.message}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Verify signature using constant-time comparison
|
|
206
|
+
def verify_signature(signature_header, expected_signature)
|
|
207
|
+
signatures = signature_header.split(" ")
|
|
208
|
+
signatures.each do |sig|
|
|
209
|
+
parts = sig.split(",", 2)
|
|
210
|
+
next if parts.length != 2
|
|
211
|
+
|
|
212
|
+
received_signature = parts[1]
|
|
213
|
+
return true if secure_compare(expected_signature, received_signature)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
raise "No matching signature found"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Generate HMAC-SHA256 signature and return it as base64
|
|
220
|
+
def generate_signature(secret, content)
|
|
221
|
+
digest = OpenSSL::HMAC.digest("sha256", secret, content)
|
|
222
|
+
Base64.strict_encode64(digest)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Constant-time string comparison to prevent timing attacks
|
|
226
|
+
#
|
|
227
|
+
# Note: We implement this manually for Ruby 2.7 compatibility.
|
|
228
|
+
# Ruby 3.0+ could use OpenSSL.fixed_length_secure_compare instead.
|
|
229
|
+
def secure_compare(str_a, str_b)
|
|
230
|
+
return false if str_a.nil? || str_b.nil? || str_a.bytesize != str_b.bytesize
|
|
231
|
+
|
|
232
|
+
bytes_a = str_a.unpack("C*")
|
|
233
|
+
result = 0
|
|
234
|
+
str_b.each_byte.with_index { |byte_b, i| result |= byte_b ^ bytes_a[i] }
|
|
235
|
+
result.zero?
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/email_fuse.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Version
|
|
4
|
+
require "email_fuse/version"
|
|
5
|
+
|
|
6
|
+
# Utils
|
|
7
|
+
require "httparty"
|
|
8
|
+
require "json"
|
|
9
|
+
require "cgi"
|
|
10
|
+
require "email_fuse/errors"
|
|
11
|
+
require "email_fuse/response"
|
|
12
|
+
require "email_fuse/client"
|
|
13
|
+
require "email_fuse/request"
|
|
14
|
+
require "email_fuse/pagination_helper"
|
|
15
|
+
|
|
16
|
+
# API Operations
|
|
17
|
+
require "email_fuse/batch"
|
|
18
|
+
require "email_fuse/emails"
|
|
19
|
+
require "email_fuse/emails/receiving"
|
|
20
|
+
require "email_fuse/emails/attachments"
|
|
21
|
+
require "email_fuse/emails/receiving/attachments"
|
|
22
|
+
require "email_fuse/webhooks"
|
|
23
|
+
|
|
24
|
+
# Rails
|
|
25
|
+
require "email_fuse/railtie" if defined?(Rails) && defined?(ActionMailer)
|
|
26
|
+
|
|
27
|
+
# Main EmailFuse module
|
|
28
|
+
module EmailFuse
|
|
29
|
+
class << self
|
|
30
|
+
attr_accessor :api_key
|
|
31
|
+
attr_writer :base_url
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield self if block_given?
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
alias config configure
|
|
38
|
+
|
|
39
|
+
def base_url
|
|
40
|
+
@base_url ||= "https://api.emailfuse.net"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/emailfuse.rb
CHANGED
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "faraday/multipart"
|
|
5
|
-
|
|
6
|
-
require "emailfuse/version"
|
|
7
|
-
|
|
8
|
-
module Emailfuse
|
|
9
|
-
autoload :Configuration, "emailfuse/configuration"
|
|
10
|
-
autoload :Client, "emailfuse/client"
|
|
11
|
-
autoload :Collection, "emailfuse/collection"
|
|
12
|
-
autoload :Error, "emailfuse/error"
|
|
13
|
-
autoload :Object, "emailfuse/object"
|
|
14
|
-
|
|
15
|
-
autoload :Deliverer, "emailfuse/deliverer"
|
|
16
|
-
|
|
17
|
-
class << self
|
|
18
|
-
attr_writer :config
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.configure
|
|
22
|
-
yield(config) if block_given?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.config
|
|
26
|
-
@config ||= Configuration.new
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
autoload :Email, "emailfuse/models/email"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
require "emailfuse/railtie" if defined?(Rails::Railtie)
|
|
3
|
+
require "email_fuse"
|
metadata
CHANGED
|
@@ -1,81 +1,68 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: emailfuse
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dean Perry
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: httparty
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 0.21.0
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 0.21.0
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
27
|
+
name: rails
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
|
-
- - "
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '2.0'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '2.0'
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: faraday-multipart
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - "~>"
|
|
30
|
+
- - ">="
|
|
46
31
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
48
|
-
type: :
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
49
34
|
prerelease: false
|
|
50
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
36
|
requirements:
|
|
52
|
-
- - "
|
|
37
|
+
- - ">="
|
|
53
38
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
55
|
-
|
|
56
|
-
email:
|
|
57
|
-
- dean@voupe.com
|
|
39
|
+
version: '0'
|
|
40
|
+
email: dean@voupe.com
|
|
58
41
|
executables: []
|
|
59
42
|
extensions: []
|
|
60
43
|
extra_rdoc_files: []
|
|
61
44
|
files:
|
|
45
|
+
- CHANGELOG.md
|
|
62
46
|
- README.md
|
|
63
|
-
-
|
|
47
|
+
- lib/email_fuse.rb
|
|
48
|
+
- lib/email_fuse/batch.rb
|
|
49
|
+
- lib/email_fuse/client.rb
|
|
50
|
+
- lib/email_fuse/emails.rb
|
|
51
|
+
- lib/email_fuse/emails/attachments.rb
|
|
52
|
+
- lib/email_fuse/emails/receiving.rb
|
|
53
|
+
- lib/email_fuse/emails/receiving/attachments.rb
|
|
54
|
+
- lib/email_fuse/errors.rb
|
|
55
|
+
- lib/email_fuse/mailer.rb
|
|
56
|
+
- lib/email_fuse/pagination_helper.rb
|
|
57
|
+
- lib/email_fuse/railtie.rb
|
|
58
|
+
- lib/email_fuse/request.rb
|
|
59
|
+
- lib/email_fuse/response.rb
|
|
60
|
+
- lib/email_fuse/version.rb
|
|
61
|
+
- lib/email_fuse/webhooks.rb
|
|
64
62
|
- lib/emailfuse.rb
|
|
65
|
-
- lib/emailfuse/client.rb
|
|
66
|
-
- lib/emailfuse/collection.rb
|
|
67
|
-
- lib/emailfuse/configuration.rb
|
|
68
|
-
- lib/emailfuse/deliverer.rb
|
|
69
|
-
- lib/emailfuse/error.rb
|
|
70
|
-
- lib/emailfuse/models/email.rb
|
|
71
|
-
- lib/emailfuse/object.rb
|
|
72
|
-
- lib/emailfuse/railtie.rb
|
|
73
|
-
- lib/emailfuse/version.rb
|
|
74
|
-
homepage:
|
|
75
63
|
licenses:
|
|
76
64
|
- MIT
|
|
77
65
|
metadata: {}
|
|
78
|
-
post_install_message:
|
|
79
66
|
rdoc_options: []
|
|
80
67
|
require_paths:
|
|
81
68
|
- lib
|
|
@@ -83,15 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
83
70
|
requirements:
|
|
84
71
|
- - ">="
|
|
85
72
|
- !ruby/object:Gem::Version
|
|
86
|
-
version: '
|
|
73
|
+
version: '2.6'
|
|
87
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
75
|
requirements:
|
|
89
76
|
- - ">="
|
|
90
77
|
- !ruby/object:Gem::Version
|
|
91
78
|
version: '0'
|
|
92
79
|
requirements: []
|
|
93
|
-
rubygems_version:
|
|
94
|
-
signing_key:
|
|
80
|
+
rubygems_version: 4.0.3
|
|
95
81
|
specification_version: 4
|
|
96
|
-
summary: Ruby
|
|
82
|
+
summary: The Ruby and Rails SDK for EmailFuse
|
|
97
83
|
test_files: []
|
data/Rakefile
DELETED
data/lib/emailfuse/client.rb
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
module Emailfuse
|
|
2
|
-
class Client
|
|
3
|
-
class << self
|
|
4
|
-
def url
|
|
5
|
-
Emailfuse.config.host || "https://app.emailfuse.net"
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def connection
|
|
9
|
-
@connection ||= Faraday.new("#{url}/api/v1") do |conn|
|
|
10
|
-
conn.request :authorization, :Bearer, Emailfuse.config.token
|
|
11
|
-
|
|
12
|
-
conn.headers = {
|
|
13
|
-
"User-Agent" => "emailfuse/v#{VERSION} (github.com/voupe/emailfuse-gem)"
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
conn.request :multipart
|
|
17
|
-
conn.request :json
|
|
18
|
-
|
|
19
|
-
conn.response :json
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def get_request(url, params: {}, headers: {})
|
|
24
|
-
handle_response connection.get(url, params, headers)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def post_request(url, body: {}, headers: {})
|
|
28
|
-
handle_response connection.post(url, body, headers)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def patch_request(url, body:, headers: {})
|
|
32
|
-
handle_response connection.patch(url, body, headers)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def delete_request(url, headers: {})
|
|
36
|
-
handle_response connection.delete(url, headers)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def handle_response(response)
|
|
40
|
-
case response.status
|
|
41
|
-
when 400
|
|
42
|
-
raise Error, "Error 400: Your request was malformed."
|
|
43
|
-
when 401
|
|
44
|
-
raise Error, "Error 401: You did not supply valid authentication credentials."
|
|
45
|
-
when 403
|
|
46
|
-
raise Error, "Error 403: You are not allowed to perform that action."
|
|
47
|
-
when 404
|
|
48
|
-
raise Error, "Error 404: No results were found for your request."
|
|
49
|
-
when 409
|
|
50
|
-
raise Error, "Error 409: Your request was a conflict."
|
|
51
|
-
when 429
|
|
52
|
-
raise Error, "Error 429: Your request exceeded the API rate limit."
|
|
53
|
-
when 422
|
|
54
|
-
raise Error, "Error 422: Unprocessable Entity."
|
|
55
|
-
when 500
|
|
56
|
-
raise Error, "Error 500: We were unable to perform the request due to server-side problems."
|
|
57
|
-
when 503
|
|
58
|
-
raise Error, "Error 503: You have been rate limited for sending more than 20 requests per second."
|
|
59
|
-
when 501
|
|
60
|
-
raise Error, "Error 501: This resource has not been implemented."
|
|
61
|
-
when 204
|
|
62
|
-
return true
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
response
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
data/lib/emailfuse/collection.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
module Emailfuse
|
|
2
|
-
class Collection
|
|
3
|
-
attr_reader :data, :total
|
|
4
|
-
|
|
5
|
-
def self.from_response(response, type:, key: nil)
|
|
6
|
-
body = response.body
|
|
7
|
-
|
|
8
|
-
if key.is_a?(String)
|
|
9
|
-
data = body["data"][key].map { |attrs| type.new(attrs) }
|
|
10
|
-
total = body["data"]["total"]
|
|
11
|
-
else
|
|
12
|
-
data = body["data"].map { |attrs| type.new(attrs) }
|
|
13
|
-
total = body["data"].count
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
new(
|
|
17
|
-
data: data,
|
|
18
|
-
total: total
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def initialize(data:, total:)
|
|
23
|
-
@data = data
|
|
24
|
-
@total = total
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
data/lib/emailfuse/deliverer.rb
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
module Emailfuse
|
|
2
|
-
class Deliverer
|
|
3
|
-
class Attachment < StringIO
|
|
4
|
-
attr_reader :original_filename, :content_type, :path
|
|
5
|
-
|
|
6
|
-
def initialize (attachment, *rest)
|
|
7
|
-
@path = ""
|
|
8
|
-
@original_filename = attachment.filename
|
|
9
|
-
@content_type = attachment.content_type.split(";")[0]
|
|
10
|
-
super attachment.body.decoded
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_accessor :settings
|
|
15
|
-
|
|
16
|
-
def initialize(settings)
|
|
17
|
-
self.settings = settings
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def api_token
|
|
21
|
-
self.settings[:token] || Emailfuse.config.token
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def deliver!(rails_message)
|
|
25
|
-
attributes = {
|
|
26
|
-
from: rails_message[:from],
|
|
27
|
-
to: rails_message[:to].formatted,
|
|
28
|
-
subject: rails_message.subject,
|
|
29
|
-
html: extract_html(rails_message),
|
|
30
|
-
text: extract_text(rails_message)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
[ :reply_to, :cc ].each do |key|
|
|
34
|
-
attributes[key] = rails_message[key].formatted if rails_message[key]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
unless rails_message.attachments.empty?
|
|
38
|
-
attributes[:attachments] = []
|
|
39
|
-
rails_message.attachments.each do |attachment|
|
|
40
|
-
attributes[:attachments] << Attachment.new(attachment, encoding: "ascii-8bit")
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
Email.create(**attributes)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# @see http://stackoverflow.com/questions/4868205/rails-mail-getting-the-body-as-plain-text
|
|
50
|
-
def extract_html(rails_message)
|
|
51
|
-
if rails_message.html_part
|
|
52
|
-
rails_message.html_part.body.decoded
|
|
53
|
-
else
|
|
54
|
-
rails_message.content_type =~ /text\/html/ ? rails_message.body.decoded : nil
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def extract_text(rails_message)
|
|
59
|
-
if rails_message.multipart?
|
|
60
|
-
rails_message.text_part ? rails_message.text_part.body.decoded : nil
|
|
61
|
-
else
|
|
62
|
-
rails_message.content_type =~ /text\/plain/ ? rails_message.body.decoded : nil
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def email_fuse_client
|
|
67
|
-
@email_fuse_client ||= Client.new(api_token, host)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
ActionMailer::Base.add_delivery_method :email_fuse, Emailfuse::Deliverer
|
data/lib/emailfuse/error.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Emailfuse
|
|
2
|
-
class Email < Object
|
|
3
|
-
class << self
|
|
4
|
-
def create(to:, from:, subject:, html: nil, text: nil, **attributes)
|
|
5
|
-
raise ArgumentError, "You must provide either html or text" if html.nil? && text.nil?
|
|
6
|
-
|
|
7
|
-
attrs = attributes.merge!(
|
|
8
|
-
to: to,
|
|
9
|
-
from: from,
|
|
10
|
-
subject: subject,
|
|
11
|
-
html: html,
|
|
12
|
-
text: text
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
response = Client.post_request("emails", body: attrs)
|
|
16
|
-
|
|
17
|
-
Email.new(response.body) if response.success?
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
data/lib/emailfuse/object.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
require "ostruct"
|
|
2
|
-
|
|
3
|
-
module Emailfuse
|
|
4
|
-
class Object < OpenStruct
|
|
5
|
-
def initialize(attributes)
|
|
6
|
-
super to_ostruct(attributes)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def to_ostruct(obj)
|
|
10
|
-
if obj.is_a?(Hash)
|
|
11
|
-
OpenStruct.new(obj.map { |key, val| [ key, to_ostruct(val) ] }.to_h)
|
|
12
|
-
elsif obj.is_a?(Array)
|
|
13
|
-
obj.map { |o| to_ostruct(o) }
|
|
14
|
-
else # Assumed to be a primitive value
|
|
15
|
-
obj
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|