sendgrid-actionmailer 0.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,136 +1,224 @@
1
1
  require 'sendgrid_actionmailer/version'
2
2
  require 'sendgrid_actionmailer/railtie' if defined? Rails
3
-
4
- require 'fileutils'
5
- require 'tmpdir'
6
-
7
3
  require 'sendgrid-ruby'
8
4
 
9
5
  module SendGridActionMailer
10
6
  class DeliveryMethod
11
- attr_reader :client
7
+ # TODO: use custom class to customer excpetion payload
8
+ SendgridDeliveryError = Class.new(StandardError)
12
9
 
13
- def initialize(params)
14
- @client = SendGrid::Client.new do |c|
15
- c.api_user = params[:api_user]
16
- c.api_key = params[:api_key]
17
- end
10
+ include SendGrid
11
+
12
+ DEFAULTS = {
13
+ raise_delivery_errors: false,
14
+ }
15
+
16
+ attr_accessor :settings
17
+
18
+ def initialize(**params)
19
+ self.settings = DEFAULTS.merge(params)
18
20
  end
19
21
 
20
22
  def deliver!(mail)
21
- attachment_temp_dirs = []
22
- from = mail[:from].addrs.first
23
-
24
- email = SendGrid::Mail.new do |m|
25
- m.to = mail[:to].addresses
26
- m.cc = mail[:cc].addresses if mail[:cc]
27
- m.bcc = mail[:bcc].addresses if mail[:bcc]
28
- m.from = from.address
29
- m.from_name = from.display_name
30
- m.reply_to = mail[:reply_to].addresses.first if mail[:reply_to]
31
- m.date = mail[:date].to_s if mail[:date]
32
- m.subject = mail.subject
33
- end
34
-
35
- smtpapi = mail['X-SMTPAPI']
36
-
37
- # If multiple X-SMTPAPI headers are present on the message, then pick the
38
- # first one. This may happen when X-SMTPAPI is set with defaults at the
39
- # class-level (using defaults()), as well as inside an individual method
40
- # (using headers[]=). In this case, we'll defer to the more specific
41
- # header set in the individual method, which is the first header
42
- # (somewhat counter-intuitively:
43
- # https://github.com/rails/rails/issues/15912).
44
- if(smtpapi.kind_of?(Array))
45
- smtpapi = smtpapi.first
46
- end
47
-
48
- if smtpapi && smtpapi.value
49
- begin
50
- data = JSON.parse(smtpapi.value)
51
-
52
- if data['filters']
53
- email.smtpapi.set_filters(data['filters'])
54
- end
23
+ sendgrid_mail = Mail.new.tap do |m|
24
+ m.from = to_email(mail.from)
25
+ m.reply_to = to_email(mail.reply_to)
26
+ m.subject = mail.subject
27
+ # https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html
28
+ m.add_personalization(to_personalizations(mail))
29
+ end
55
30
 
56
- if data['category']
57
- email.smtpapi.set_categories(data['category'])
58
- end
31
+ add_content(sendgrid_mail, mail)
32
+ add_send_options(sendgrid_mail, mail)
33
+ add_mail_settings(sendgrid_mail, mail)
34
+ add_tracking_settings(sendgrid_mail, mail)
59
35
 
60
- if data['send_at']
61
- email.smtpapi.set_send_at(data['send_at'])
62
- end
36
+ response = perform_send_request(sendgrid_mail)
63
37
 
64
- if data['send_each_at']
65
- email.smtpapi.set_send_each_at(data['send_each_at'])
66
- end
38
+ settings[:return_response] ? response : self
39
+ end
67
40
 
68
- if data['section']
69
- email.smtpapi.set_sections(data['section'])
70
- end
41
+ private
71
42
 
72
- if data['sub']
73
- email.smtpapi.set_substitutions(data['sub'])
74
- end
43
+ def client
44
+ @client ||= SendGrid::API.new(api_key: settings.fetch(:api_key)).client
45
+ end
75
46
 
76
- if data['asm_group_id']
77
- email.smtpapi.set_asm_group(data['asm_group_id'])
78
- end
47
+ # type should be either :plain or :html
48
+ def to_content(type, value)
49
+ Content.new(type: "text/#{type}", value: value)
50
+ end
79
51
 
80
- if data['unique_args']
81
- email.smtpapi.set_unique_args(data['unique_args'])
82
- end
52
+ def to_email(input)
53
+ to_emails(input).first
54
+ end
83
55
 
84
- if data['ip_pool']
85
- email.smtpapi.set_ip_pool(data['ip_pool'])
86
- end
87
- rescue JSON::ParserError
88
- raise ArgumentError, "X-SMTPAPI is not JSON: #{smtpapi.value}"
56
+ def to_emails(input)
57
+ if input.is_a?(String)
58
+ [Email.new(email: input)]
59
+ elsif input.is_a?(::Mail::AddressContainer) && !input.instance_variable_get('@field').nil?
60
+ input.instance_variable_get('@field').addrs.map do |addr| # Mail::Address
61
+ Email.new(email: addr.address, name: addr.name)
89
62
  end
63
+ elsif input.is_a?(::Mail::AddressContainer)
64
+ input.map do |addr|
65
+ Email.new(email: addr)
66
+ end
67
+ elsif input.is_a?(::Mail::StructuredField)
68
+ [Email.new(email: input.value)]
69
+ elsif input.nil?
70
+ []
71
+ else
72
+ puts "unknown type #{input.class.name}"
90
73
  end
74
+ end
75
+
76
+ def to_personalizations(mail)
77
+ Personalization.new.tap do |p|
78
+ to_emails(mail.to).each { |to| p.add_to(to) }
79
+ to_emails(mail.cc).each { |cc| p.add_cc(cc) }
80
+ to_emails(mail.bcc).each { |bcc| p.add_bcc(bcc) }
81
+ p.add_substitution(Substitution.new(key: "%asm_group_unsubscribe_raw_url%", value: "<%asm_group_unsubscribe_raw_url%>"))
82
+ p.add_substitution(Substitution.new(key: "%asm_global_unsubscribe_raw_url%", value: "<%asm_global_unsubscribe_raw_url%>"))
83
+ p.add_substitution(Substitution.new(key: "%asm_preferences_raw_url%", value: "<%asm_preferences_raw_url%>"))
84
+ end
85
+ end
86
+
87
+ def to_attachment(part)
88
+ Attachment.new.tap do |a|
89
+ a.content = Base64.strict_encode64(part.body.decoded)
90
+ a.type = part.mime_type
91
+ a.filename = part.filename
91
92
 
92
- # TODO: This is pretty ugly
93
+ disposition = get_disposition(part)
94
+ a.disposition = disposition unless disposition.nil?
95
+
96
+ has_content_id = part.header && part.has_content_id?
97
+ a.content_id = part.header['content_id'].value if has_content_id
98
+ end
99
+ end
100
+
101
+ def get_disposition(message)
102
+ return if message.header.nil?
103
+ content_disp = message.header[:content_disposition]
104
+ return unless content_disp.respond_to?(:disposition_type)
105
+ content_disp.disposition_type
106
+ end
107
+
108
+ def add_content(sendgrid_mail, mail)
93
109
  case mail.mime_type
94
110
  when 'text/plain'
95
- # Text
96
- email.text = mail.body.decoded
111
+ sendgrid_mail.add_content(to_content(:plain, mail.body.decoded))
97
112
  when 'text/html'
98
- # HTML
99
- email.html = mail.body.decoded
113
+ sendgrid_mail.add_content(to_content(:html, mail.body.decoded))
100
114
  when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
101
- email.html = mail.html_part.decoded if mail.html_part
102
- email.text = mail.text_part.decoded if mail.text_part
103
-
104
- mail.attachments.each do |a|
105
- # Write the attachment into a temporary location, since sendgrid-ruby
106
- # expects to deal with files.
107
- #
108
- # We write to a temporary directory (instead of a tempfile) and then
109
- # use the original filename inside there, since sendgrid-ruby's
110
- # add_content method pulls the filename from the path (so tempfiles
111
- # would lead to random filenames).
112
- temp_dir = Dir.mktmpdir('sendgrid-actionmailer')
113
- attachment_temp_dirs << temp_dir
114
- temp_path = File.join(temp_dir, a.filename)
115
- File.open(temp_path, 'wb') do |file|
116
- file.write(a.read)
115
+ sendgrid_mail.add_content(to_content(:plain, mail.text_part.decoded)) if mail.text_part
116
+ sendgrid_mail.add_content(to_content(:html, mail.html_part.decoded)) if mail.html_part
117
+
118
+ mail.attachments.each do |part|
119
+ sendgrid_mail.add_attachment(to_attachment(part))
120
+ end
121
+ end
122
+ end
123
+
124
+ def json_parse(text, symbolize=true)
125
+ JSON.parse(text.gsub(/:*\"*([\%a-zA-Z0-9_-]*)\"*(( *)=>\ *)/) { "\"#{$1}\":" }, symbolize_names: symbolize)
126
+ end
127
+
128
+ def add_send_options(sendgrid_mail, mail)
129
+ if mail['template_id']
130
+ sendgrid_mail.template_id = mail['template_id'].to_s
131
+ end
132
+ if mail['sections']
133
+ json_parse(mail['sections'].value, false).each do |key, value|
134
+ sendgrid_mail.add_section(Section.new(key: key, value: value))
135
+ end
136
+ end
137
+ if mail['headers']
138
+ json_parse(mail['headers'].value, false).each do |key, value|
139
+ sendgrid_mail.add_header(Header.new(key: key, value: value))
140
+ end
141
+ end
142
+ if mail['categories']
143
+ mail['categories'].value.split(",").each do |value|
144
+ sendgrid_mail.add_category(Category.new(name: value.strip))
145
+ end
146
+ end
147
+ if mail['custom_args']
148
+ json_parse(mail['custom_args'].value, false).each do |key, value|
149
+ sendgrid_mail.add_custom_arg(CustomArg.new(key: key, value: value))
150
+ end
151
+ end
152
+ if mail['send_at'] && mail['batch_id']
153
+ sendgrid_mail.send_at = mail['send_at'].value.to_i
154
+ sendgrid_mail.batch_id= mail['batch_id'].to_s
155
+ end
156
+ if mail['asm']
157
+ asm = json_parse(mail['asm'].value)
158
+ asm = asm.delete_if { |key, value| !key.to_s.match(/(group_id)|(groups_to_display)/) }
159
+ if asm[:group_id]
160
+ sendgrid_mail.asm = ASM.new(asm)
161
+ end
162
+ end
163
+ if mail['ip_pool_name']
164
+ sendgrid_mail.ip_pool_name = mail['ip_pool_name'].to_s
165
+ end
166
+ end
167
+
168
+ def add_mail_settings(sendgrid_mail, mail)
169
+ if mail['mail_settings']
170
+ settings = json_parse(mail['mail_settings'].value)
171
+ sendgrid_mail.mail_settings = MailSettings.new.tap do |m|
172
+ if settings[:bcc]
173
+ m.bcc = BccSettings.new(settings[:bcc])
174
+ end
175
+ if settings[:bypass_list_management]
176
+ m.bypass_list_management = BypassListManagement.new(settings[:bypass_list_management])
177
+ end
178
+ if settings[:footer]
179
+ m.footer = Footer.new(settings[:footer])
117
180
  end
181
+ if settings[:sandbox_mode]
182
+ m.sandbox_mode = SandBoxMode.new(settings[:sandbox_mode])
183
+ end
184
+ if settings[:spam_check]
185
+ m.spam_check = SpamCheck.new(settings[:spam_check])
186
+ end
187
+ end
188
+ end
189
+ end
118
190
 
119
- if(mail.mime_type == 'multipart/related' && a.header[:content_id])
120
- email.add_content(temp_path, a.header[:content_id].field.content_id)
121
- else
122
- email.add_attachment(temp_path, a.filename)
191
+ def add_tracking_settings(sendgrid_mail, mail)
192
+ if mail['tracking_settings']
193
+ settings = json_parse(mail['tracking_settings'].value)
194
+ sendgrid_mail.tracking_settings = TrackingSettings.new.tap do |t|
195
+ if settings[:click_tracking]
196
+ t.click_tracking = ClickTracking.new(settings[:click_tracking])
197
+ end
198
+ if settings[:open_tracking]
199
+ t.open_tracking = OpenTracking.new(settings[:open_tracking])
200
+ end
201
+ if settings[:subscription_tracking]
202
+ t.subscription_tracking = SubscriptionTracking.new(settings[:subscription_tracking])
203
+ end
204
+ if settings[:ganalytics]
205
+ t.ganalytics = Ganalytics.new(settings[:ganalytics])
123
206
  end
124
207
  end
125
208
  end
209
+ end
210
+
211
+ def perform_send_request(email)
212
+ result = client.mail._('send').post(request_body: email.to_json) # ლ(ಠ益ಠლ) that API
126
213
 
127
- client.send(email)
128
- ensure
129
- # Close and delete the attachment tempfiles after the e-mail has been
130
- # sent.
131
- attachment_temp_dirs.each do |dir|
132
- FileUtils.remove_entry_secure(dir)
214
+ if result.status_code && result.status_code.start_with?('4')
215
+ message = JSON.parse(result.body).fetch('errors').pop.fetch('message')
216
+ full_message = "Sendgrid delivery failed with #{result.status_code} #{message}"
217
+
218
+ settings[:raise_delivery_errors] ? raise(SendgridDeliveryError, full_message) : warn(full_message)
133
219
  end
220
+
221
+ result
134
222
  end
135
223
  end
136
224
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'sendgrid_actionmailer/version'
@@ -19,11 +20,11 @@ Gem::Specification.new do |spec|
19
20
  spec.require_paths = ['lib']
20
21
 
21
22
  spec.add_dependency 'mail', '~> 2.5'
22
- spec.add_dependency 'sendgrid-ruby', '< 2.0'
23
+ spec.add_dependency 'sendgrid-ruby', '~> 5.0'
23
24
 
25
+ spec.add_development_dependency 'appraisal', '~> 2.1.0'
24
26
  spec.add_development_dependency 'bundler', '~> 1.6'
25
27
  spec.add_development_dependency 'rake'
26
- spec.add_development_dependency 'rspec', '~>3.2.0'
27
- spec.add_development_dependency 'appraisal', '~> 2.1.0'
28
- spec.add_development_dependency 'webmock', '~> 1.24.6'
28
+ spec.add_development_dependency 'rspec', '~> 3.2'
29
+ spec.add_development_dependency 'webmock'
29
30
  end