mailjet 1.5.4 → 1.8.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.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +298 -183
  3. data/Rakefile +23 -14
  4. data/lib/mailjet/configuration.rb +56 -4
  5. data/lib/mailjet/connection.rb +68 -33
  6. data/lib/mailjet/exception/errors.rb +94 -0
  7. data/lib/mailjet/mailer.rb +45 -16
  8. data/lib/mailjet/rack/endpoint.rb +2 -3
  9. data/lib/mailjet/resource.rb +105 -72
  10. data/lib/mailjet/resources/campaigndraft.rb +1 -1
  11. data/lib/mailjet/resources/campaigndraft_detailcontent.rb +17 -1
  12. data/lib/mailjet/resources/campaigndraft_schedule.rb +1 -1
  13. data/lib/mailjet/resources/campaigndraft_send.rb +1 -1
  14. data/lib/mailjet/resources/campaigndraft_status.rb +1 -1
  15. data/lib/mailjet/resources/campaigndraft_test.rb +1 -1
  16. data/lib/mailjet/resources/contact.rb +11 -2
  17. data/lib/mailjet/resources/contact_getcontactslists.rb +17 -0
  18. data/lib/mailjet/resources/contact_pii.rb +8 -0
  19. data/lib/mailjet/resources/contactslist_csv.rb +8 -0
  20. data/lib/mailjet/resources/csvimport.rb +0 -1
  21. data/lib/mailjet/resources/listrecipient.rb +2 -1
  22. data/lib/mailjet/resources/messagehistory.rb +16 -0
  23. data/lib/mailjet/resources/messageinformation.rb +17 -0
  24. data/lib/mailjet/resources/newsletter.rb +1 -2
  25. data/lib/mailjet/resources/openinformation.rb +17 -0
  26. data/lib/mailjet/resources/retrieve_errors_csv.rb +8 -0
  27. data/lib/mailjet/resources/send.rb +1 -1
  28. data/lib/mailjet/resources/statcounters.rb +33 -0
  29. data/lib/mailjet/resources/statistics_linkclick.rb +12 -0
  30. data/lib/mailjet/resources/template_detailcontent.rb +18 -2
  31. data/lib/mailjet/version.rb +1 -1
  32. data/lib/mailjet.rb +4 -6
  33. metadata +25 -120
  34. data/lib/mailjet/api_error.rb +0 -29
  35. data/lib/mailjet/core_extensions/ostruct.rb +0 -9
  36. data/lib/mailjet/gem_extensions/rest_client.rb +0 -19
@@ -1,17 +1,69 @@
1
- require 'active_support/core_ext/module/attribute_accessors'
2
-
3
1
  module Mailjet
4
2
  module Configuration
5
- mattr_accessor :api_key, :secret_key, :default_from
3
+ def self.api_key
4
+ @api_key
5
+ end
6
+
7
+ def self.api_key=(api_key)
8
+ @api_key = api_key
9
+ end
10
+
11
+ def self.secret_key
12
+ @secret_key
13
+ end
14
+
15
+ def self.secret_key=(secret_key)
16
+ @secret_key = secret_key
17
+ end
18
+
19
+ def self.default_from
20
+ @default_from
21
+ end
22
+
23
+ def self.default_from=(default_from)
24
+ @default_from = default_from
25
+ end
26
+
27
+ def self.api_version
28
+ @api_version
29
+ end
30
+
31
+ def self.api_version=(api_version)
32
+ @api_version = api_version
33
+ end
34
+
35
+ def self.sandbox_mode
36
+ @sandbox_mode
37
+ end
38
+
39
+ def self.sandbox_mode=(sandbox_mode)
40
+ @sandbox_mode = sandbox_mode
41
+ end
42
+
43
+ def self.end_point
44
+ @end_point
45
+ end
46
+
47
+ def self.end_point=(end_point)
48
+ @end_point = end_point
49
+ end
50
+
51
+ def self.perform_api_call
52
+ @perform_api_call
53
+ end
54
+
55
+ def self.perform_api_call=(perform_api_call)
56
+ @perform_api_call = perform_api_call
57
+ end
6
58
 
7
59
  DEFAULT = {
8
60
  api_version: 'v3',
61
+ sandbox_mode: false,
9
62
  end_point: 'https://api.mailjet.com',
10
63
  perform_api_call: true,
11
64
  }
12
65
 
13
66
  DEFAULT.each do |param, default_value|
14
- mattr_accessor param
15
67
  self.send("#{param}=", default_value)
16
68
  end
17
69
  end
@@ -1,42 +1,32 @@
1
- require 'rest_client'
2
- require 'mailjet/gem_extensions/rest_client'
3
- require 'active_support/core_ext/module/delegation'
4
- require 'json'
1
+ require 'faraday'
2
+ require 'yajl'
5
3
 
6
4
  module Mailjet
7
5
  class Connection
8
6
 
9
- attr_accessor :adapter, :public_operations, :read_only, :perform_api_call
7
+ attr_accessor :adapter, :public_operations, :read_only, :perform_api_call, :api_key, :secret_key, :options
10
8
  alias :read_only? :read_only
11
9
 
12
- delegate :options, :concat_urls, :url, to: :adapter
13
-
14
10
  def [](suburl, &new_block)
15
- broken_url = url.split("/")
11
+ broken_url = uri.path.split("/")
16
12
  if broken_url.include?("contactslist") && broken_url.include?("managemanycontacts") && broken_url.last.to_i > 0
17
- self.class.new(url, options[:user], options[:password], options)
13
+ self.class.new(uri, api_key, secret_key, options)
18
14
  else
19
- self.class.new(concat_urls(url, suburl), options[:user], options[:password], options)
15
+ self.class.new(concat_urls(suburl), api_key, secret_key, options)
20
16
  end
21
17
  end
22
18
 
23
19
  def initialize(end_point, api_key, secret_key, options = {})
24
- # #charles proxy
25
- # RestClient.proxy = "http://127.0.0.1:8888"
26
- # #
27
- # #Output for debugging
28
- # RestClient.log =
29
- # Object.new.tap do |proxy|
30
- # def proxy.<<(message)
31
- # Rails.logger.info message
32
- # end
33
- # end
34
- # #
35
- adapter_class = options[:adapter_class] || RestClient::Resource
20
+ self.options = options
21
+ self.api_key = api_key
22
+ self.secret_key = secret_key
36
23
  self.public_operations = options[:public_operations] || []
37
24
  self.read_only = options[:read_only]
38
- # self.adapter = adapter_class.new(end_point, options.merge(user: api_key, password: secret_key, :verify_ssl => false, content_type: 'application/json'))
39
- self.adapter = adapter_class.new(end_point, options.merge(user: api_key, password: secret_key, content_type: 'application/json'))
25
+ self.adapter = Faraday.new(end_point, ssl: { verify: false }) do |conn|
26
+ conn.response :raise_error, include_request: true
27
+ conn.request :authorization, :basic, api_key, secret_key
28
+ conn.headers['Content-Type'] = 'application/json'
29
+ end
40
30
  self.perform_api_call = options.key?(:perform_api_call) ? options[:perform_api_call] : true
41
31
  end
42
32
 
@@ -56,22 +46,31 @@ module Mailjet
56
46
  handle_api_call(:delete, additional_headers, &block)
57
47
  end
58
48
 
49
+ def concat_urls(suburl)
50
+ self.adapter.build_url(suburl.to_s)
51
+ end
52
+
53
+ def uri
54
+ self.adapter.build_url
55
+ end
56
+
59
57
  private
60
58
 
61
59
  def handle_api_call(method, additional_headers = {}, payload = {}, &block)
62
- formatted_payload = (additional_headers[:content_type] == :json) ? payload.to_json : payload
60
+ formatted_payload = (additional_headers["Content-Type"] == 'application/json') ? Yajl::Encoder.encode(payload) : payload
63
61
  raise Mailjet::MethodNotAllowed unless method_allowed(method)
64
62
 
65
63
  if self.perform_api_call
66
64
  if [:get, :delete].include?(method)
67
- @adapter.send(method, additional_headers, &block)
65
+ @adapter.send(method, nil, additional_headers[:params], &block)
68
66
  else
69
- @adapter.send(method, formatted_payload, additional_headers, &block)
67
+ @adapter.send(method, nil, formatted_payload, additional_headers, &block)
70
68
  end
71
69
  else
72
- return {'Count' => 0, 'Data' => [mock_api_call: true], 'Total' => 0}.to_json
70
+ return Yajl::Encoder.encode({'Count' => 0, 'Data' => [mock_api_call: true], 'Total' => 0})
73
71
  end
74
- rescue RestClient::Exception => e
72
+
73
+ rescue Faraday::Error => e
75
74
  handle_exception(e, additional_headers, formatted_payload)
76
75
  end
77
76
 
@@ -81,16 +80,52 @@ module Mailjet
81
80
  end
82
81
 
83
82
  def handle_exception(e, additional_headers, payload = {})
83
+ return e.response_body if e.response_headers[:content_type].include?("text/plain")
84
+
84
85
  params = additional_headers[:params] || {}
85
- formatted_payload = (additional_headers[:content_type] == :json) ? JSON.parse(payload) : payload
86
- params = params.merge(formatted_payload)
86
+ formatted_payload = (additional_headers[:content_type] == :json) ? Yajl::Parser.parse(payload) : payload
87
+ params = params.merge!(formatted_payload) if formatted_payload.is_a?(Hash)
88
+
89
+ response_body = if e.response_headers[:content_type].include?("application/json")
90
+ e.response_body
91
+ else
92
+ "{}"
93
+ end
87
94
 
88
- raise Mailjet::ApiError.new(e.http_code, e.http_body, @adapter, @adapter.url, params)
95
+ if sent_invalid_email?(e.response_body, @adapter.build_url)
96
+ return e.response_body
97
+ else
98
+ raise communication_error(e)
99
+ end
100
+ end
101
+
102
+ def communication_error(e)
103
+ if e.respond_to?(:response) && e.response
104
+ return case e.response_status
105
+ when Unauthorized::CODE
106
+ Unauthorized.new(e.message, e)
107
+ when BadRequest::CODE
108
+ BadRequest.new(e.message, e)
109
+ else
110
+ CommunicationError.new(e.message, e)
111
+ end
112
+ end
113
+ CommunicationError.new(e.message)
114
+ end
115
+
116
+ def sent_invalid_email?(error_http_body, uri)
117
+ return false unless uri.path.include?('v3.1/send')
118
+ return unless error_http_body
119
+
120
+ parsed_body = Yajl::Parser.parse(error_http_body)
121
+ error_message = parsed_body.dig('Messages')&.first&.dig('Errors')&.first&.dig('ErrorMessage') || []
122
+ error_message.include?('is an invalid email address.')
123
+ rescue
124
+ false
89
125
  end
90
126
 
91
127
  end
92
128
 
93
129
  class MethodNotAllowed < StandardError
94
-
95
130
  end
96
131
  end
@@ -0,0 +1,94 @@
1
+ require 'yajl'
2
+ require 'json'
3
+
4
+ module Mailjet
5
+ class Error < StandardError
6
+ attr_reader :object
7
+
8
+ def initialize(message = nil, object = nil)
9
+ super(message)
10
+ @object = object
11
+ end
12
+ end
13
+
14
+ class ApiError < StandardError
15
+ attr_reader :code, :reason
16
+
17
+ # @param code [Integer] HTTP response status code
18
+ # @param body [String] JSON response body
19
+ # @param request [Object] any request object
20
+ # @param url [String] request URL
21
+ # @param params [Hash] request headers and parameters
22
+ def initialize(code, body, request, url, params)
23
+ @code = code
24
+ @reason = begin
25
+ resdec = JSON.parse(body)
26
+ resdec['ErrorMessage']
27
+ rescue JSON::ParserError
28
+ body
29
+ end
30
+
31
+ if request.respond_to?(:options)
32
+ request.options[:user] = '***'
33
+ request.options[:password] = '***'
34
+ end
35
+
36
+ message = "error #{code} while sending #{request.inspect} to #{url} with #{params.inspect}"
37
+ error_details = body.inspect
38
+ hint = "Please see https://dev.mailjet.com/email/reference/overview/errors/ for more informations on error numbers."
39
+
40
+ super("#{message}\n\n#{error_details}\n\n#{hint}\n\n")
41
+ end
42
+ end
43
+
44
+ class CommunicationError < Error
45
+ attr_reader :code
46
+
47
+ NOCODE = 000
48
+
49
+ def initialize(message = nil, response = nil)
50
+ @response = response
51
+ @code = if response.nil?
52
+ NOCODE
53
+ else
54
+ response.response_status
55
+ end
56
+
57
+ api_message = begin
58
+ Yajl::Parser.parse(response.response_body)['ErrorMessage']
59
+ rescue Yajl::ParseError
60
+ response.response_body
61
+ rescue NoMethodError
62
+ "Unknown API error"
63
+ rescue
64
+ 'Unknown API error'
65
+ end
66
+
67
+ message ||= ''
68
+ api_message ||= ''
69
+ message = message + ': ' + api_message
70
+
71
+ super(message, response)
72
+ rescue NoMethodError, JSON::ParserError
73
+ @code = NOCODE
74
+ super(message, response)
75
+ end
76
+ end
77
+
78
+ class Unauthorized < CommunicationError
79
+ CODE = 401
80
+
81
+ def initialize(error_message, response)
82
+ error_message = error_message + ' - Invalid Domain or API key'
83
+ super(error_message, response)
84
+ end
85
+ end
86
+
87
+ class BadRequest < CommunicationError
88
+ CODE = 400
89
+
90
+ def initialize(error_message, response)
91
+ super(error_message, response)
92
+ end
93
+ end
94
+ end
@@ -1,7 +1,8 @@
1
1
  require 'action_mailer'
2
2
  require 'mail'
3
3
  require 'base64'
4
- require 'json'
4
+ require 'yajl'
5
+
5
6
 
6
7
  # Mailjet::Mailer enables to send a Mail::Message via Mailjet SMTP relay servers
7
8
  # User is the API key, and password the API secret
@@ -15,7 +16,7 @@ class Mailjet::Mailer < ::Mail::SMTP
15
16
  user_name: options.delete(:api_key) || Mailjet.config.api_key,
16
17
  password: options.delete(:secret_key) || Mailjet.config.secret_key,
17
18
  enable_starttls_auto: true
18
- }.merge(options))
19
+ }.merge!(options))
19
20
  end
20
21
  end
21
22
 
@@ -42,6 +43,13 @@ class Mailjet::APIMailer
42
43
 
43
44
  CONNECTION_PERMITTED_OPTIONS = [:api_key, :secret_key]
44
45
 
46
+ HEADER_BLACKLIST = [
47
+ 'from', 'sender', 'subject', 'to', 'cc', 'bcc', 'return-path', 'delivered-to', 'dkim-signature',
48
+ 'domainkey-status', 'received-spf', 'authentication-results', 'received', 'user-agent', 'x-mailer',
49
+ 'x-feedback-id', 'list-id', 'date', 'x-csa-complaints', 'message-id', 'reply-to', 'content-type',
50
+ 'mime-version', 'content-transfer-encoding'
51
+ ]
52
+
45
53
  def initialize(opts = {})
46
54
  options = HashWithIndifferentAccess.new(opts)
47
55
 
@@ -67,7 +75,7 @@ class Mailjet::APIMailer
67
75
  version = options[:version] || Mailjet.config.api_version
68
76
 
69
77
  if (version == 'v3.1')
70
- Mailjet::Send.create({ :Messages => [setContentV3_1(mail)] }, options)
78
+ Mailjet::Send.create({ :Messages => [setContentV3_1(mail)], SandboxMode: Mailjet.config.sandbox_mode }, options)
71
79
  else
72
80
  Mailjet::Send.create(setContentV3_0(mail), options)
73
81
  end
@@ -110,7 +118,7 @@ class Mailjet::APIMailer
110
118
  if mail.header && mail.header.fields.any?
111
119
  content[:Headers] = {}
112
120
  mail.header.fields.each do |header|
113
- if header.name.start_with?('X-') && !header.name.start_with?('X-MJ') && !header.name.start_with?('X-Mailjet')
121
+ if !header.name.start_with?('X-MJ') && !header.name.start_with?('X-Mailjet') && !HEADER_BLACKLIST.include?(header.name.downcase)
114
122
  content[:Headers][header.name] = header.value
115
123
  end
116
124
  end
@@ -119,8 +127,8 @@ class Mailjet::APIMailer
119
127
  # ReplyTo property was added in v3.1
120
128
  # Passing it as an header if mail.reply_to
121
129
 
122
- if mail.reply_to
123
- if mail.reply_to.respond_to?(:display_names) && mail.reply_to.display_names.first
130
+ if mail[:reply_to]
131
+ if mail[:reply_to].respond_to?(:display_names) && mail[:reply_to].display_names.first
124
132
  content[:ReplyTo] = {:Email=> mail[:reply_to].addresses.first, :Name=> mail[:reply_to].display_names.first}
125
133
  else
126
134
  content[:ReplyTo] = {:Email=> mail[:reply_to].addresses.first}
@@ -163,8 +171,13 @@ class Mailjet::APIMailer
163
171
  ccs =[{:Email=>mail[:cc].address.first}]
164
172
  end
165
173
  else
174
+ ccs = []
166
175
  mail[:cc].each do |cc|
167
- ccs << {:Email=> cc.address, :Name=>cc.display_name}
176
+ if cc.display_name
177
+ ccs << {:Email=> cc.address, :Name=>cc.display_name}
178
+ else
179
+ ccs << {:Email=> cc.address}
180
+ end
168
181
  end
169
182
  end
170
183
  end
@@ -177,7 +190,8 @@ class Mailjet::APIMailer
177
190
  payload[:Bcc] = [{:Email=>mail[:bcc].address.first}]
178
191
  end
179
192
  else
180
- mail[:bcc].formatted.each do |bcc|
193
+ bccs = []
194
+ mail[:bcc].each do |bcc|
181
195
  if bcc.display_name
182
196
  bccs << {:Email=> bcc.address, :Name=>bcc.display_name}
183
197
  else
@@ -189,16 +203,33 @@ class Mailjet::APIMailer
189
203
 
190
204
  payload = {
191
205
  :To=> to,
192
- }.merge(content)
193
- .merge(base_from)
194
- .merge(@delivery_method_options_v3_1)
206
+ }.merge!(content, base_from, @delivery_method_options_v3_1)
195
207
 
196
208
  payload[:Subject] = mail.subject if !mail.subject.blank?
197
209
  payload[:Sender] = mail[:sender] if !mail[:sender].blank?
198
210
  payload[:Cc] = ccs if mail[:cc]
199
211
  payload[:Bcc] = bccs if mail[:bcc]
200
212
 
201
- payload
213
+ decode_emails_V3_1!(payload)
214
+ end
215
+
216
+ def decode_emails_V3_1!(payload)
217
+ # ActionMailer may have handed us encoded email
218
+ # addresses, mailjet will reject. Therefore we
219
+ # walk through the payload to decode them back.
220
+ payload.each do |key, value|
221
+ if key == :Email
222
+ payload[key] = Mail::Encodings.value_decode(value)
223
+ elsif value.is_a?(Hash)
224
+ decode_emails_V3_1! value
225
+ elsif value.is_a?(Array)
226
+ value.each do |item|
227
+ if item.is_a?(Hash)
228
+ decode_emails_V3_1! item
229
+ end
230
+ end
231
+ end
232
+ end
202
233
  end
203
234
 
204
235
  def setContentV3_0(mail)
@@ -262,10 +293,8 @@ class Mailjet::APIMailer
262
293
  payload[:bcc] = mail[:bcc].formatted.join(', ') if mail[:bcc]
263
294
 
264
295
  # Send the final payload to Mailjet Send API
265
- payload.merge(content)
266
- .merge(base_from)
267
- .merge(@delivery_method_options_v3_0)
268
- end
296
+ payload.merge!(content, base_from, @delivery_method_options_v3_0)
297
+ end
269
298
  end
270
299
 
271
300
  ActionMailer::Base.add_delivery_method :mailjet_api, Mailjet::APIMailer
@@ -1,6 +1,5 @@
1
- require 'active_support'
2
1
  require 'rack/request'
3
-
2
+ require 'yajl'
4
3
 
5
4
  module Mailjet
6
5
  module Rack
@@ -13,7 +12,7 @@ module Mailjet
13
12
 
14
13
  def call(env)
15
14
  if env['PATH_INFO'] == @path && (content = env['rack.input'].read)
16
- @block.call(ActiveSupport::JSON.decode(content))
15
+ @block.call(Yajl::Parser.parse(content))
17
16
  [200, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
18
17
  else
19
18
  @app.call(env)