mailjet 1.5.0 → 1.6.0

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