mailjet 1.5.0 → 1.6.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.
data/Rakefile CHANGED
@@ -1,17 +1,22 @@
1
- #!/usr/bin/env rake
2
- require 'rake/testtask'
3
- require 'bundler'
4
- Bundler::GemHelper.install_tasks
1
+ require "rspec/core/rake_task"
5
2
 
6
- $:.push File.expand_path("../lib", __FILE__)
7
-
8
- Rake::TestTask.new(:spec) do |t|
9
- t.libs << 'lib'
10
- t.libs << 'spec'
11
- # t.pattern = 'spec/**/*_spec.rb'
12
- t.verbose = true
13
- t.test_files = Dir['spec/**/*_spec.rb']
3
+ RSpec::Core::RakeTask.new(:spec) do |t|
4
+ t.pattern = [
5
+ "spec/mailjet/api_error_spec.rb",
6
+ "spec/mailjet/apikey_spec.rb",
7
+ "spec/mailjet/mailer_spec.rb",
8
+ "spec/mailjet/resource_spec.rb",
9
+ "spec/configuration_spec.rb",
10
+ "spec/mailjet_spec.rb",
11
+ "spec/resources/contact_spec.rb",
12
+ "spec/resources/contactmetadata_spec.rb",
13
+ "spec/resources/messagehistory_spec.rb",
14
+ "spec/resources/getcontactslists_spec.rb",
15
+ "spec/resources/template_detailcontent_spec.rb",
16
+ "spec/resources/integration_spec.rb",
17
+ "spec/resources/newsletter_spec.rb",
18
+ "spec/resources/statcounters_spec.rb",
19
+ ]
14
20
  end
15
21
 
16
-
17
- task :default => :spec
22
+ task default: [:spec]
@@ -0,0 +1,35 @@
1
+ module Mailjet
2
+ class InitializerGenerator < Rails::Generators::Base
3
+ desc 'This generator creates an initializer file mailjet.rb at config/initializers'
4
+
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def generate_initializer_file
8
+ config_file_path = 'config/initializers/mailjet.rb'
9
+
10
+ say('Hey! We’re about to configure your Mailjet credentials for your application.')
11
+ say('You can find them on your account (https://app.mailjet.com/account/api_keys).')
12
+ say('Please help yourself by providing some intel:')
13
+
14
+ @api_key = ask('API key: ')
15
+ @secret_key = ask('Secret key: ')
16
+ @default_from = ask('Sender address:')
17
+
18
+ say("Don't forget that your sender address '#{@default_from}' has to be validated first on https://app.mailjet.com/account/sender.")
19
+
20
+ if @api_v3_1 = yes?('Do you want to use Mailjet API v3.1 for sending your emails? (y/n)')
21
+ @api_v3_1_notice = %{
22
+ Mailjet API v3.1 is at the moment limited to Send API.
23
+ We’ve not set the version to it directly since there is no other endpoint in that version.
24
+ We recommend you create a dedicated instance of the wrapper set with it to send your emails.
25
+ If you're only using the gem to send emails, then you can safely set it to this version.
26
+ Otherwise, you can remove the dedicated line into #{config_file_path}.
27
+
28
+ }
29
+ say(@api_v3_1_notice)
30
+ end
31
+
32
+ template 'mailjet.rb.erb', config_file_path
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ # kindly generated by appropriated Rails generator
2
+ Mailjet.configure do |config|
3
+ config.api_key = '<%= @api_key %>'
4
+ config.secret_key = '<%= @secret_key %>'
5
+ config.default_from = '<%= @default_from %>'
6
+ <% if @api_v3_1 %><%= @api_v3_1_notice.split("\n").reject(&:empty?).map{ |l| ' # ' + l }.join("\n") %>
7
+ config.api_version = 'v3.1'<% end %>
8
+ end
@@ -1,29 +1,28 @@
1
- # encoding: utf-8
2
- require 'active_support'
1
+ require "json"
3
2
 
4
3
  module Mailjet
5
4
  class ApiError < StandardError
5
+ attr_reader :code, :reason
6
6
 
7
- attr_accessor :code, :reason
7
+ # @param code [Integer] HTTP response status code
8
+ # @param body [String] JSON response body
9
+ # @param request [Object] any request object
10
+ # @param url [String] request URL
11
+ # @param params [Hash] request headers and parameters
12
+ def initialize(code, body, request, url, params)
13
+ @code = code
14
+ @reason = begin
15
+ resdec = JSON.parse(body)
16
+ resdec['ErrorMessage']
17
+ rescue JSON::ParserError
18
+ body
19
+ end
8
20
 
21
+ message = "error #{code} while sending #{request.inspect} to #{url} with #{params.inspect}"
22
+ error_details = body.inspect
23
+ hint = "Please see https://dev.mailjet.com/guides/#status-codes for more informations on error numbers."
9
24
 
10
- def initialize(code, res, request, request_path, params)
11
- self.code = code
12
- self.reason = ""
13
- unless res.blank?
14
- resdec = ActiveSupport::JSON.decode(res)
15
- self.reason = resdec['ErrorMessage']
16
- end
17
- # code is ugly, output is pretty
18
- super("error #{code} while sending #{request.inspect} to #{request_path} with #{params.inspect}\n\n" +
19
- if res['errors'].present?
20
- [(res['errors'] || [])].flatten.map do |param, text|
21
- [param, text].map(&:to_s).reject(&:blank?).join(': ')
22
- end.join("\n")
23
- else
24
- res.inspect
25
- end + "\n\nPlease see https://dev.mailjet.com/guides/#status-codes for more informations on error numbers.\n\n"
26
- )
25
+ super("#{message}\n\n#{error_details}\n\n#{hint}\n\n")
27
26
  end
28
27
  end
29
28
  end
@@ -2,18 +2,18 @@ require 'active_support/core_ext/module/attribute_accessors'
2
2
 
3
3
  module Mailjet
4
4
  module Configuration
5
- mattr_accessor :api_key
6
- mattr_accessor :secret_key
7
- mattr_accessor :default_from
8
- mattr_accessor :api_version do
9
- 'v3'
10
- end
11
- mattr_accessor :end_point do
12
- 'https://api.mailjet.com'
13
- end
14
- mattr_accessor :perform_api_call do
15
- true
16
- end
5
+ mattr_accessor :api_key, :secret_key, :default_from
17
6
 
7
+ DEFAULT = {
8
+ api_version: 'v3',
9
+ sandbox_mode: false,
10
+ end_point: 'https://api.mailjet.com',
11
+ perform_api_call: true,
12
+ }
13
+
14
+ DEFAULT.each do |param, default_value|
15
+ mattr_accessor param
16
+ self.send("#{param}=", default_value)
17
+ end
18
18
  end
19
19
  end
@@ -6,7 +6,7 @@ require 'json'
6
6
  module Mailjet
7
7
  class Connection
8
8
 
9
- attr_accessor :adapter, :public_operations, :read_only
9
+ attr_accessor :adapter, :public_operations, :read_only, :perform_api_call, :read_timeout, :open_timeout
10
10
  alias :read_only? :read_only
11
11
 
12
12
  delegate :options, :concat_urls, :url, to: :adapter
@@ -35,38 +35,43 @@ module Mailjet
35
35
  adapter_class = options[:adapter_class] || RestClient::Resource
36
36
  self.public_operations = options[:public_operations] || []
37
37
  self.read_only = options[:read_only]
38
+ self.read_timeout = options[:read_timeout]
39
+ self.open_timeout = options[:open_timeout]
38
40
  # 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'))
41
+ self.adapter = adapter_class.new(end_point, options.merge(user: api_key, password: secret_key, content_type: 'application/json', read_timeout: self.read_timeout, open_timeout: self.open_timeout))
42
+ self.perform_api_call = options.key?(:perform_api_call) ? options[:perform_api_call] : true
40
43
  end
41
44
 
42
- def get(additional_headers = {}, perform_api_call, &block)
43
- handle_api_call(:get, additional_headers, perform_api_call, &block)
45
+ def get(additional_headers = {}, &block)
46
+ handle_api_call(:get, additional_headers, &block)
44
47
  end
45
48
 
46
- def post(payload, additional_headers = {}, perform_api_call, &block)
47
- handle_api_call(:post, additional_headers, payload, perform_api_call, &block)
49
+ def post(payload, additional_headers = {}, &block)
50
+ handle_api_call(:post, additional_headers, payload, &block)
48
51
  end
49
52
 
50
- def put(payload, additional_headers = {}, perform_api_call, &block)
51
- handle_api_call(:put, additional_headers, payload, perform_api_call, &block)
53
+ def put(payload, additional_headers = {}, &block)
54
+ handle_api_call(:put, additional_headers, payload, &block)
52
55
  end
53
56
 
54
- def delete(additional_headers = {}, perform_api_call, &block)
55
- handle_api_call(:delete, additional_headers, perform_api_call, &block)
57
+ def delete(additional_headers = {}, &block)
58
+ handle_api_call(:delete, additional_headers, &block)
56
59
  end
57
60
 
58
61
  private
59
62
 
60
- def handle_api_call(method, additional_headers = {}, payload = {}, perform_api_call, &block)
63
+ def handle_api_call(method, additional_headers = {}, payload = {}, &block)
61
64
  formatted_payload = (additional_headers[:content_type] == :json) ? payload.to_json : payload
62
65
  raise Mailjet::MethodNotAllowed unless method_allowed(method)
63
66
 
64
- if perform_api_call
67
+ if self.perform_api_call
65
68
  if [:get, :delete].include?(method)
66
69
  @adapter.send(method, additional_headers, &block)
67
70
  else
68
71
  @adapter.send(method, formatted_payload, additional_headers, &block)
69
72
  end
73
+ else
74
+ return {'Count' => 0, 'Data' => [mock_api_call: true], 'Total' => 0}.to_json
70
75
  end
71
76
  rescue RestClient::Exception => e
72
77
  handle_exception(e, additional_headers, formatted_payload)
@@ -82,7 +87,13 @@ module Mailjet
82
87
  formatted_payload = (additional_headers[:content_type] == :json) ? JSON.parse(payload) : payload
83
88
  params = params.merge(formatted_payload)
84
89
 
85
- raise Mailjet::ApiError.new(e.http_code, e.http_body, @adapter, @adapter.url, params)
90
+ http_body = if e.http_headers[:content_type] == "application/json"
91
+ e.http_body
92
+ else
93
+ "{}"
94
+ end
95
+
96
+ raise Mailjet::ApiError.new(e.http_code, http_body, @adapter, @adapter.url, params)
86
97
  end
87
98
 
88
99
  end
@@ -26,60 +26,75 @@ ActionMailer::Base.add_delivery_method :mailjet, Mailjet::Mailer
26
26
  # Mailjet sends API expects a JSON payload as the input.
27
27
  # The deliver methods maps the Mail::Message attributes to the MailjetSend API JSON expected structure
28
28
  class Mailjet::APIMailer
29
- def initialize(options = {})
30
- # send = Mailjet.Send.new
31
- # if send.version.exists
32
- # version = send.version
33
- # else
34
- @version = Mailjet.config.api_version
35
- # end
36
- @delivery_method_options_v3_0 = options.slice(
37
- :api_key, :secret_key,
38
- :recipients, :'mj-prio', :'mj-campaign', :'mj-deduplicatecampaign',
39
- :'mj-templatelanguage', :'mj-templateerrorreporting', :'mj-templateerrordeliver', :'mj-templateid',
40
- :'mj-trackopen', :'mj-trackclick',
41
- :'mj-customid', :'mj-eventpayload', :vars, :headers,
42
- )
43
- @delivery_method_options_v3_1 = options.slice(
44
- :api_key, :secret_key,
45
- :'Priority', :'CustomCampaign', :'DeduplicateCampaign',
46
- :'TemplateLanguage', :'TemplateErrorReporting', :'TemplateErrorDeliver', :'TemplateID',
47
- :'TrackOpens', :'TrackClicks',
48
- :'CustomID', :'EventPayload', :'Variables', :'Headers',
49
- )
29
+ V3_0_PERMITTED_OPTIONS = [
30
+ :recipients, :'mj-prio', :'mj-campaign', :'mj-deduplicatecampaign',
31
+ :'mj-templatelanguage', :'mj-templateerrorreporting', :'mj-templateerrordeliver', :'mj-templateid',
32
+ :'mj-trackopen', :'mj-trackclick',
33
+ :'mj-customid', :'mj-eventpayload', :vars, :headers,
34
+ ]
35
+
36
+ V3_1_PERMITTED_OPTIONS = [
37
+ :'Priority', :'CustomCampaign', :'DeduplicateCampaign',
38
+ :'TemplateLanguage', :'TemplateErrorReporting', :'TemplateErrorDeliver', :'TemplateID',
39
+ :'TrackOpens', :'TrackClicks',
40
+ :'CustomID', :'EventPayload', :'Variables', :'Headers',
41
+ ]
42
+
43
+ CONNECTION_PERMITTED_OPTIONS = [:api_key, :secret_key]
44
+
45
+ HEADER_BLACKLIST = [
46
+ 'from', 'sender', 'subject', 'to', 'cc', 'bcc', 'return-path', 'delivered-to', 'dkim-signature',
47
+ 'domainkey-status', 'received-spf', 'authentication-results', 'received', 'user-agent', 'x-mailer',
48
+ 'x-feedback-id', 'list-id', 'date', 'x-csa-complaints', 'message-id', 'reply-to', 'content-type',
49
+ 'mime-version', 'content-transfer-encoding'
50
+ ]
51
+
52
+ def initialize(opts = {})
53
+ options = HashWithIndifferentAccess.new(opts)
54
+
55
+ @version = options[:version]
56
+ @delivery_method_options_v3_0 = options.slice(*V3_0_PERMITTED_OPTIONS)
57
+ @delivery_method_options_v3_1 = options.slice(*V3_1_PERMITTED_OPTIONS)
58
+ @connection_options = options.slice(*CONNECTION_PERMITTED_OPTIONS)
50
59
  end
51
60
 
52
- def deliver!(mail, options = nil)
53
- # p mail.header.fields
61
+ def deliver!(mail, opts = {})
62
+ options = HashWithIndifferentAccess.new(opts)
54
63
 
55
- if (options && options.kind_of?(Object) && options['version'].present?)
56
- @version = options['version']
57
- end
64
+ # Mailjet Send API does not support full from. Splitting the from field into two: name and email address
65
+ mail[:from] ||= Mailjet.config.default_from if Mailjet.config.default_from
58
66
 
59
- if (!options.kind_of?(Object))
60
- options = []
61
- end
67
+ # add `@connection_options` in `options` only if not exist yet (values in `options` prime)
68
+ options.reverse_merge!(@connection_options)
62
69
 
63
- # Mailjet Send API does not support full from. Splitting the from field into two: name and email address
64
- if mail[:from].nil? && Mailjet.config.default_from.present?
65
- mail[:from] = Mailjet.config.default_from
66
- end
70
+ # add `@version` in options if set
71
+ options[:version] = @version if @version
67
72
 
68
- if (@version == 'v3.1')
69
- Mailjet::Send.create({:Messages => [setContentV3_1(mail)]})
73
+ # `options[:version]` primes on global config
74
+ version = options[:version] || Mailjet.config.api_version
75
+
76
+ if (version == 'v3.1')
77
+ Mailjet::Send.create({ :Messages => [setContentV3_1(mail)], SandboxMode: Mailjet.config.sandbox_mode }, options)
70
78
  else
71
- Mailjet::Send.create(setContentV3_0(mail))
79
+ Mailjet::Send.create(setContentV3_0(mail), options)
72
80
  end
73
81
  end
74
82
 
75
83
  def setContentV3_1(mail)
76
84
  content = {}
85
+
77
86
  content[:TextPart] = mail.text_part.try(:decoded) if !mail.text_part.blank?
78
87
  content[:HTMLPart] = mail.html_part.try(:decoded) if !mail.html_part.blank?
79
88
 
89
+ # try message `body` as fallback if no content found
90
+ unless content[:TextPart] || content[:HTMLPart] || mail.body.try(:raw_source).empty?
91
+ content[mail.content_type.try(:include?,'text/html') ? :HTMLPart : :TextPart] = mail.body.raw_source
92
+ end
93
+
94
+
80
95
  if mail.attachments.any?
81
96
  content[:Attachments] = []
82
- content[:InlineAttachments] = []
97
+ content[:InlinedAttachments] = []
83
98
 
84
99
  mail.attachments.each do |attachment|
85
100
  mailjet_attachment = {
@@ -90,7 +105,7 @@ class Mailjet::APIMailer
90
105
 
91
106
  if attachment.inline?
92
107
  mailjet_attachment['ContentId'] = attachment.content_id
93
- content[:InlineAttachments].push(mailjet_attachment)
108
+ content[:InlinedAttachments].push(mailjet_attachment)
94
109
  else
95
110
  content[:Attachments].push(mailjet_attachment)
96
111
  end
@@ -102,20 +117,20 @@ class Mailjet::APIMailer
102
117
  if mail.header && mail.header.fields.any?
103
118
  content[:Headers] = {}
104
119
  mail.header.fields.each do |header|
105
- if header.name.start_with?('X-') && !header.name.start_with?('X-MJ') && !header.name.start_with?('X-Mailjet')
120
+ if !header.name.start_with?('X-MJ') && !header.name.start_with?('X-Mailjet') && !HEADER_BLACKLIST.include?(header.name.downcase)
106
121
  content[:Headers][header.name] = header.value
107
122
  end
108
123
  end
109
124
  end
110
125
 
111
- # Reply-To is not a property in Mailjet Send API
126
+ # ReplyTo property was added in v3.1
112
127
  # Passing it as an header if mail.reply_to
113
128
 
114
- if mail.reply_to
115
- if mail.reply_to.display_names.first
116
- content[:Headers]['Reply-To'] = {:Email=> mail[:reply_to].addresses.first, :Name=> mail[:reply_to].display_names.first}
129
+ if mail[:reply_to]
130
+ if mail[:reply_to].respond_to?(:display_names) && mail[:reply_to].display_names.first
131
+ content[:ReplyTo] = {:Email=> mail[:reply_to].addresses.first, :Name=> mail[:reply_to].display_names.first}
117
132
  else
118
- content[:Headers]['Reply-To'] = {:Email=> mail[:reply_to].addresses.first}
133
+ content[:ReplyTo] = {:Email=> mail[:reply_to].addresses.first}
119
134
  end
120
135
  end
121
136
 
@@ -155,8 +170,13 @@ class Mailjet::APIMailer
155
170
  ccs =[{:Email=>mail[:cc].address.first}]
156
171
  end
157
172
  else
173
+ ccs = []
158
174
  mail[:cc].each do |cc|
159
- ccs << {:Email=> cc.address, :Name=>cc.display_name}
175
+ if cc.display_name
176
+ ccs << {:Email=> cc.address, :Name=>cc.display_name}
177
+ else
178
+ ccs << {:Email=> cc.address}
179
+ end
160
180
  end
161
181
  end
162
182
  end
@@ -169,7 +189,8 @@ class Mailjet::APIMailer
169
189
  payload[:Bcc] = [{:Email=>mail[:bcc].address.first}]
170
190
  end
171
191
  else
172
- mail[:bcc].formatted.each do |bcc|
192
+ bccs = []
193
+ mail[:bcc].each do |bcc|
173
194
  if bcc.display_name
174
195
  bccs << {:Email=> bcc.address, :Name=>bcc.display_name}
175
196
  else
@@ -190,7 +211,26 @@ class Mailjet::APIMailer
190
211
  payload[:Cc] = ccs if mail[:cc]
191
212
  payload[:Bcc] = bccs if mail[:bcc]
192
213
 
193
- payload
214
+ decode_emails_V3_1!(payload)
215
+ end
216
+
217
+ def decode_emails_V3_1!(payload)
218
+ # ActionMailer may have handed us encoded email
219
+ # addresses, mailjet will reject. Therefore we
220
+ # walk through the payload to decode them back.
221
+ payload.each do |key, value|
222
+ if key == :Email
223
+ payload[key] = Mail::Encodings.value_decode(value)
224
+ elsif value.is_a?(Hash)
225
+ decode_emails_V3_1! value
226
+ elsif value.is_a?(Array)
227
+ value.each do |item|
228
+ if item.is_a?(Hash)
229
+ decode_emails_V3_1! item
230
+ end
231
+ end
232
+ end
233
+ end
194
234
  end
195
235
 
196
236
  def setContentV3_0(mail)
@@ -199,6 +239,11 @@ class Mailjet::APIMailer
199
239
  content[:text_part] = mail.text_part.try(:decoded) if !mail.text_part.blank?
200
240
  content[:html_part] = mail.html_part.try(:decoded) if !mail.html_part.blank?
201
241
 
242
+ # try message `body` as fallback if no content found
243
+ unless content[:text_part] || content[:html_part] || mail.body.try(:raw_source).empty?
244
+ content[mail.content_type.try(:include?,'text/html') ? :html_part : :text_part] = mail.body.raw_source
245
+ end
246
+
202
247
  # Formatting attachments (inline + regular)
203
248
  unless mail.attachments.empty?
204
249
  content[:attachments] = []
@@ -252,7 +297,7 @@ class Mailjet::APIMailer
252
297
  payload.merge(content)
253
298
  .merge(base_from)
254
299
  .merge(@delivery_method_options_v3_0)
255
- end
300
+ end
256
301
  end
257
302
 
258
303
  ActionMailer::Base.add_delivery_method :mailjet_api, Mailjet::APIMailer