lib/roda/plugins/error_mail.rb



# frozen-string-literal: true

require 'mail'

class Roda
  module RodaPlugins
    # The error_mail plugin adds an +error_mail+ instance method that
    # send an email related to the exception.  This is most useful if you are
    # also using the error_handler plugin:
    #
    #   plugin :error_mail, to: 'to@example.com', from: 'from@example.com'
    #   plugin :error_handler do |e|
    #     error_mail(e)
    #     'Internal Server Error'
    #   end
    #
    # It is similar to the error_email plugin, except that it uses the mail
    # library instead of net/smtp directly.  If you are already using the
    # mail library in your application, it makes sense to use error_mail
    # instead of error_email.
    #
    # Options:
    #
    # :filter :: Callable called with the key and value for each parameter, environment
    #            variable, and session value.  If it returns true, the value of the
    #            parameter is filtered in the email.
    # :from :: The From address to use in the email (required)
    # :headers :: A hash of additional headers to use in the email (default: empty hash)
    # :prefix :: A prefix to use in the email's subject line (default: no prefix)
    # :to :: The To address to use in the email (required)
    #
    # The subject of the error email shows the exception class and message.
    # The body of the error email shows the backtrace of the error and the
    # request environment, as well the request params and session variables (if any).
    # You can also call error_mail with a plain string instead of an exception,
    # in which case the string is used as the subject, and no backtrace is included.
    #
    # Note that emailing on every error as shown above is only appropriate
    # for low traffic web applications.  For high traffic web applications,
    # use an error reporting service instead of this plugin.
    module ErrorMail
      DEFAULT_FILTER = lambda{|k,v| false}
      private_constant :DEFAULT_FILTER

      # Set default opts for plugin.  See ErrorEmail module RDoc for options.
      def self.configure(app, opts=OPTS)
        app.opts[:error_mail] = email_opts = (app.opts[:error_mail] || {:filter=>DEFAULT_FILTER}).merge(opts).freeze
        unless email_opts[:to] && email_opts[:from]
          raise RodaError, "must provide :to and :from options to error_mail plugin"
        end
      end

      module InstanceMethods
        # Send an email for the given error.  +exception+ is usually an exception
        # instance, but it can be a plain string which is used as the subject for
        # the email.
        def error_mail(exception)
          _error_mail(exception).deliver!
        end

        # The content of the email to send, include the headers and the body.
        # Takes the same argument as #error_mail.
        def error_mail_content(exception)
          _error_mail(exception).to_s
        end

        private

        def _error_mail(e)
          email_opts = self.class.opts[:error_mail]
          subject = if e.respond_to?(:message)
            "#{e.class}: #{e.message}"
          else
            e.to_s
          end
          subject = "#{email_opts[:prefix]}#{subject}"
          filter = email_opts[:filter]

          format = lambda do |h|
            h = h.map{|k, v| "#{k.inspect} => #{filter.call(k, v) ? 'FILTERED' : v.inspect}"}
            h.sort!
            h.join("\n")
          end 

          begin
            params = request.params
            params = (format[params] unless params.empty?)
          rescue
            params = 'Invalid Parameters!'
          end

          message = String.new
          message << <<END
Path: #{request.path}

END
          if e.respond_to?(:backtrace)
            message << <<END
Backtrace:

#{e.backtrace.join("\n")}
END
          end

          message << <<END

ENV:

#{format[env]}
END

          if params
            message << <<END

Params:

#{params}
END
          end

          if env['rack.session']
            message << <<END

Session:

#{format[session]}
END
          end


          Mail.new do
            from email_opts[:from]
            to email_opts[:to]
            subject subject
            body message

            if headers = email_opts[:headers]
              headers.each do |k,v|
                header[k] = v
              end
            end
          end
        end
      end
    end

    register_plugin(:error_mail, ErrorMail)
  end
end