lib/sendgrid_actionmailer.rb in sendgrid-actionmailer-0.2.1 vs lib/sendgrid_actionmailer.rb in sendgrid-actionmailer-2.0.0

- old
+ new

@@ -1,136 +1,224 @@ require 'sendgrid_actionmailer/version' require 'sendgrid_actionmailer/railtie' if defined? Rails - -require 'fileutils' -require 'tmpdir' - require 'sendgrid-ruby' module SendGridActionMailer class DeliveryMethod - attr_reader :client + # TODO: use custom class to customer excpetion payload + SendgridDeliveryError = Class.new(StandardError) - def initialize(params) - @client = SendGrid::Client.new do |c| - c.api_user = params[:api_user] - c.api_key = params[:api_key] - end + include SendGrid + + DEFAULTS = { + raise_delivery_errors: false, + } + + attr_accessor :settings + + def initialize(**params) + self.settings = DEFAULTS.merge(params) end def deliver!(mail) - attachment_temp_dirs = [] - from = mail[:from].addrs.first - - email = SendGrid::Mail.new do |m| - m.to = mail[:to].addresses - m.cc = mail[:cc].addresses if mail[:cc] - m.bcc = mail[:bcc].addresses if mail[:bcc] - m.from = from.address - m.from_name = from.display_name - m.reply_to = mail[:reply_to].addresses.first if mail[:reply_to] - m.date = mail[:date].to_s if mail[:date] - m.subject = mail.subject + sendgrid_mail = Mail.new.tap do |m| + m.from = to_email(mail.from) + m.reply_to = to_email(mail.reply_to) + m.subject = mail.subject + # https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html + m.add_personalization(to_personalizations(mail)) end - smtpapi = mail['X-SMTPAPI'] + add_content(sendgrid_mail, mail) + add_send_options(sendgrid_mail, mail) + add_mail_settings(sendgrid_mail, mail) + add_tracking_settings(sendgrid_mail, mail) - # If multiple X-SMTPAPI headers are present on the message, then pick the - # first one. This may happen when X-SMTPAPI is set with defaults at the - # class-level (using defaults()), as well as inside an individual method - # (using headers[]=). In this case, we'll defer to the more specific - # header set in the individual method, which is the first header - # (somewhat counter-intuitively: - # https://github.com/rails/rails/issues/15912). - if(smtpapi.kind_of?(Array)) - smtpapi = smtpapi.first - end + response = perform_send_request(sendgrid_mail) - if smtpapi && smtpapi.value - begin - data = JSON.parse(smtpapi.value) + settings[:return_response] ? response : self + end - if data['filters'] - email.smtpapi.set_filters(data['filters']) - end + private - if data['category'] - email.smtpapi.set_categories(data['category']) - end + def client + @client ||= SendGrid::API.new(api_key: settings.fetch(:api_key)).client + end - if data['send_at'] - email.smtpapi.set_send_at(data['send_at']) - end + # type should be either :plain or :html + def to_content(type, value) + Content.new(type: "text/#{type}", value: value) + end - if data['send_each_at'] - email.smtpapi.set_send_each_at(data['send_each_at']) - end + def to_email(input) + to_emails(input).first + end - if data['section'] - email.smtpapi.set_sections(data['section']) - end + def to_emails(input) + if input.is_a?(String) + [Email.new(email: input)] + elsif input.is_a?(::Mail::AddressContainer) && !input.instance_variable_get('@field').nil? + input.instance_variable_get('@field').addrs.map do |addr| # Mail::Address + Email.new(email: addr.address, name: addr.name) + end + elsif input.is_a?(::Mail::AddressContainer) + input.map do |addr| + Email.new(email: addr) + end + elsif input.is_a?(::Mail::StructuredField) + [Email.new(email: input.value)] + elsif input.nil? + [] + else + puts "unknown type #{input.class.name}" + end + end - if data['sub'] - email.smtpapi.set_substitutions(data['sub']) - end + def to_personalizations(mail) + Personalization.new.tap do |p| + to_emails(mail.to).each { |to| p.add_to(to) } + to_emails(mail.cc).each { |cc| p.add_cc(cc) } + to_emails(mail.bcc).each { |bcc| p.add_bcc(bcc) } + p.add_substitution(Substitution.new(key: "%asm_group_unsubscribe_raw_url%", value: "<%asm_group_unsubscribe_raw_url%>")) + p.add_substitution(Substitution.new(key: "%asm_global_unsubscribe_raw_url%", value: "<%asm_global_unsubscribe_raw_url%>")) + p.add_substitution(Substitution.new(key: "%asm_preferences_raw_url%", value: "<%asm_preferences_raw_url%>")) + end + end - if data['asm_group_id'] - email.smtpapi.set_asm_group(data['asm_group_id']) - end + def to_attachment(part) + Attachment.new.tap do |a| + a.content = Base64.strict_encode64(part.body.decoded) + a.type = part.mime_type + a.filename = part.filename - if data['unique_args'] - email.smtpapi.set_unique_args(data['unique_args']) - end + disposition = get_disposition(part) + a.disposition = disposition unless disposition.nil? - if data['ip_pool'] - email.smtpapi.set_ip_pool(data['ip_pool']) - end - rescue JSON::ParserError - raise ArgumentError, "X-SMTPAPI is not JSON: #{smtpapi.value}" - end + has_content_id = part.header && part.has_content_id? + a.content_id = part.header['content_id'].value if has_content_id end + end - # TODO: This is pretty ugly + def get_disposition(message) + return if message.header.nil? + content_disp = message.header[:content_disposition] + return unless content_disp.respond_to?(:disposition_type) + content_disp.disposition_type + end + + def add_content(sendgrid_mail, mail) case mail.mime_type when 'text/plain' - # Text - email.text = mail.body.decoded + sendgrid_mail.add_content(to_content(:plain, mail.body.decoded)) when 'text/html' - # HTML - email.html = mail.body.decoded + sendgrid_mail.add_content(to_content(:html, mail.body.decoded)) when 'multipart/alternative', 'multipart/mixed', 'multipart/related' - email.html = mail.html_part.decoded if mail.html_part - email.text = mail.text_part.decoded if mail.text_part + sendgrid_mail.add_content(to_content(:plain, mail.text_part.decoded)) if mail.text_part + sendgrid_mail.add_content(to_content(:html, mail.html_part.decoded)) if mail.html_part - mail.attachments.each do |a| - # Write the attachment into a temporary location, since sendgrid-ruby - # expects to deal with files. - # - # We write to a temporary directory (instead of a tempfile) and then - # use the original filename inside there, since sendgrid-ruby's - # add_content method pulls the filename from the path (so tempfiles - # would lead to random filenames). - temp_dir = Dir.mktmpdir('sendgrid-actionmailer') - attachment_temp_dirs << temp_dir - temp_path = File.join(temp_dir, a.filename) - File.open(temp_path, 'wb') do |file| - file.write(a.read) + mail.attachments.each do |part| + sendgrid_mail.add_attachment(to_attachment(part)) + end + end + end + + def json_parse(text, symbolize=true) + JSON.parse(text.gsub(/:*\"*([\%a-zA-Z0-9_-]*)\"*(( *)=>\ *)/) { "\"#{$1}\":" }, symbolize_names: symbolize) + end + + def add_send_options(sendgrid_mail, mail) + if mail['template_id'] + sendgrid_mail.template_id = mail['template_id'].to_s + end + if mail['sections'] + json_parse(mail['sections'].value, false).each do |key, value| + sendgrid_mail.add_section(Section.new(key: key, value: value)) + end + end + if mail['headers'] + json_parse(mail['headers'].value, false).each do |key, value| + sendgrid_mail.add_header(Header.new(key: key, value: value)) + end + end + if mail['categories'] + mail['categories'].value.split(",").each do |value| + sendgrid_mail.add_category(Category.new(name: value.strip)) + end + end + if mail['custom_args'] + json_parse(mail['custom_args'].value, false).each do |key, value| + sendgrid_mail.add_custom_arg(CustomArg.new(key: key, value: value)) + end + end + if mail['send_at'] && mail['batch_id'] + sendgrid_mail.send_at = mail['send_at'].value.to_i + sendgrid_mail.batch_id= mail['batch_id'].to_s + end + if mail['asm'] + asm = json_parse(mail['asm'].value) + asm = asm.delete_if { |key, value| !key.to_s.match(/(group_id)|(groups_to_display)/) } + if asm[:group_id] + sendgrid_mail.asm = ASM.new(asm) + end + end + if mail['ip_pool_name'] + sendgrid_mail.ip_pool_name = mail['ip_pool_name'].to_s + end + end + + def add_mail_settings(sendgrid_mail, mail) + if mail['mail_settings'] + settings = json_parse(mail['mail_settings'].value) + sendgrid_mail.mail_settings = MailSettings.new.tap do |m| + if settings[:bcc] + m.bcc = BccSettings.new(settings[:bcc]) end + if settings[:bypass_list_management] + m.bypass_list_management = BypassListManagement.new(settings[:bypass_list_management]) + end + if settings[:footer] + m.footer = Footer.new(settings[:footer]) + end + if settings[:sandbox_mode] + m.sandbox_mode = SandBoxMode.new(settings[:sandbox_mode]) + end + if settings[:spam_check] + m.spam_check = SpamCheck.new(settings[:spam_check]) + end + end + end + end - if(mail.mime_type == 'multipart/related' && a.header[:content_id]) - email.add_content(temp_path, a.header[:content_id].field.content_id) - else - email.add_attachment(temp_path, a.filename) + def add_tracking_settings(sendgrid_mail, mail) + if mail['tracking_settings'] + settings = json_parse(mail['tracking_settings'].value) + sendgrid_mail.tracking_settings = TrackingSettings.new.tap do |t| + if settings[:click_tracking] + t.click_tracking = ClickTracking.new(settings[:click_tracking]) end + if settings[:open_tracking] + t.open_tracking = OpenTracking.new(settings[:open_tracking]) + end + if settings[:subscription_tracking] + t.subscription_tracking = SubscriptionTracking.new(settings[:subscription_tracking]) + end + if settings[:ganalytics] + t.ganalytics = Ganalytics.new(settings[:ganalytics]) + end end end + end - client.send(email) - ensure - # Close and delete the attachment tempfiles after the e-mail has been - # sent. - attachment_temp_dirs.each do |dir| - FileUtils.remove_entry_secure(dir) + def perform_send_request(email) + result = client.mail._('send').post(request_body: email.to_json) # ლ(ಠ益ಠლ) that API + + if result.status_code && result.status_code.start_with?('4') + message = JSON.parse(result.body).fetch('errors').pop.fetch('message') + full_message = "Sendgrid delivery failed with #{result.status_code} #{message}" + + settings[:raise_delivery_errors] ? raise(SendgridDeliveryError, full_message) : warn(full_message) end + + result end end end