mailjet 1.5.4 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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)