require 'snails/util' require 'erb' require 'tilt/erb' require 'tuktuk' require 'snails' module Snails class Mailer include Snails::SimpleFormat Time.include(Snails::RelativeTime) unless Time.instance_methods.include?(:relative) @queue = :emails # class Bounce < StandardError; end # class SoftBounce < Bounce; end # class HardBounce < Bounce; end def self.backends { 'test_backend' => 'Snails::Mailer::TestBackend', 'smtp' => 'Snails::Mailer::TuktukBackend', 'mailgun' => 'Snails::Mailer::MailgunBackend' } end def self.init_backend(backend_name, opts = {}) backend_name = backend_name.presence || 'test_backend' backends[backend_name].constantize.new(opts) end def initialize(opts) @from_email = opts[:from] or raise ":from required" @base_subject = opts[:base_subject] || '' @views = opts[:views] || Snails.root.join('lib', 'views') @logfile = opts[:logfile] # || Snails.root.join('log', 'mailer.log') @backend = self.class.init_backend(opts[:backend_name], opts[:backend_options]) end def email(name, &block) define_singleton_method(name) do |*args| instance_exec(*args, &block) end end def helpers(&block) instance_eval(&block) end def perform(method, *args) send(method, *args) end # e.g. Notifier.queue(:some_notification, @project, "arg1") def queue(method, obj, *args) return unless Snails.env.production? Resque.enqueue(self, method, obj.id, *args) end def send_error(to: @from_email, err:, env:, params: {}) @exception, @env, @params = err, env, params @url = "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']}" subject = "#{@url} :: (#{@exception.class}) \"#{@exception.message}\"" content = %{ A <%= @exception.class %> occurred in <%= @url %>: ----------------- <%= @exception.message %> <%= @exception.backtrace.join("\n") %> ----------------- - Request : <%= @url %> - Parameters : <%= @params.inspect %> - IP address : <%= @env['REMOTE_ADDR'] %> - User agent : <%= @env['HTTP_USER_AGENT'] %> - Process : <%= $$ %> - Server : <%= `hostname -s`.chomp %> ----------------- }.strip send_email(to: to, subject: subject, body: content) end private def render(view_path, layout_path = nil) view = File.read(File.join(@views, "#{view_path}.erb")) if layout_path layout = File.read(File.join(@views, "#{layout_path}.erb")) render_erb(layout) { render_erb(view) } else render_erb(view) end end def render_erb(template) ERB.new(template).result(binding) end def partial(name) partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}" render(partial_name) end def logger @logger ||= @logfile ? Logger.new(@logfile) : Snails.logger end def send_email(from: @from_email, to:, subject:, body: nil, template: nil, layout: nil, html_body: nil, html_template: nil, html_layout: nil, message_id: nil, attachments: nil, return_path: nil, list_unsubscribe: nil) raise "No recipient given for mail: #{subject}!" if to.blank? message = { to: to, from: from, subject: @base_subject + subject } raise "Blank from" if message[:from].blank? raise "Blank to" if message[:to].blank? if body or template message[:body] = template ? render(template, layout) : body end if html_body or html_template message[:html_body] = html_template ? render(html_template, html_layout) : html_body end message[:attachments] = attachments if attachments message[:message_id] = message_id if message_id message[:return_path] = return_path if return_path message[:list_unsubscribe] = list_unsubscribe if list_unsubscribe logger.info "[#{to}] Delivering: #{subject}" @backend.deliver(message) end alias_method :deliver, :send_email class Backend def deliver(email, options = {}) raise "Please redefine in subclass" end def deliver_many(emails, options = {}) emails.map { |e| deliver(e, options) } end end class TestBackend < Backend def initialize(opts = {}) end def deliver(email, options = {}) puts "Deliverying single: #{email[:to]}" end def deliver_many(emails, options = {}) puts "Deliverying many: #{emails.count}" end end class TuktukBackend < Backend def initialize(config = {}) @debug = config[:debug] puts "-- #{config.inspect}" if key = config.dig(:dkim, :private_key) and File.exist?(key) config[:dkim][:private_key] = IO.read(key) elsif config[:dkim] puts "Private key for DKIM not found! Disabling..." config.delete(:dkim) end Tuktuk.options = config end def deliver(email, options = {}) debug = @debug.nil? ? !Snails.env.production? : @debug # if debug isn't set, determine based on env resp, email = Tuktuk.deliver(email, debug: debug) if resp.is_a?(Tuktuk::Bounce) puts "[#{to}] Email bounced! [#{resp.code}] #{resp.message}" end return resp, email end def deliver_many(emails, options = {}) Tuktuk.deliver_many(emails, options) end end class MailgunBackend < Backend def initialize(api_key:, domain_name:) @key = api_key @url = "https://api.mailgun.net/v3/#{domain_name}/messages" end def deliver(email, options = {}) raise "No body!" if email[:body].nil? data = { from: email[:from], to: email[:to], subject: email[:subject], } data[:text] = email[:body] if email[:body] data[:html] = email[:html_body] if email[:html_body] if data[:text].blank? && data[:html].blank? raise ArgumentError, "Either text or html required" end opts = { username: 'api', password: @key } resp = Dagger.post(@url, data, opts) resp.ok? ? [resp.data, data[:to]] : nil end end end end