sendgrid-actionmailer 0.2.1 → 2.0.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.
@@ -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