diff --git a/lib/logging/appenders/email.rb b/lib/logging/appenders/email.rb index abc09da0..55758b93 100644 --- a/lib/logging/appenders/email.rb +++ b/lib/logging/appenders/email.rb @@ -2,73 +2,168 @@ require 'net/smtp' require 'time' # get rfc822 time format -# a replacement EmailOutputter. This is essentially the default EmailOutputter from Log4r but with the following -# changes: -# 1) if there is data to send in an email, then do not send anything -# 2) connect to the smtp server at the last minute, do not connect at startup and then send later on. -# 3) Fix the To: field so that it looks alright. module Logging::Appenders -class Email < ::Logging::Appender - include Buffering - - attr_reader :server, :port, :domain, :acct, :authtype, :tls, :subject - - # TODO: make the from/to fields modifiable - # possibly the subject, too - - def initialize( name, opts = {} ) - super(name, opts) - - af = opts.getopt(:buffsize) || - opts.getopt(:buffer_size) || - 100 - configure_buffering({:auto_flushing => af}.merge(opts)) - - # get the SMTP parameters - @from = opts.getopt(:from) - raise ArgumentError, 'Must specify from address' if @from.nil? - - @to = opts.getopt(:to, '').split(',') - raise ArgumentError, 'Must specify recipients' if @to.empty? - - @server = opts.getopt :server, 'localhost' - @port = opts.getopt :port, 25, :as => Integer - @domain = opts.getopt(:domain, ENV['HOSTNAME']) || 'localhost.localdomain' - @acct = opts.getopt :acct - @passwd = opts.getopt :passwd - @authtype = opts.getopt :authtype, :cram_md5, :as => Symbol - @tls = opts.getopt :tls, false - @subject = opts.getopt :subject, "Message of #{$0}" - @params = [@server, @port, @domain, @acct, @passwd, @authtype] - end - - -private - - # This method is called by the buffering code when messages need to be - # sent out as an email. + # Provides an appender that can send log messages via email to a list of + # recipients. # - def canonical_write( str ) - ### build a mail header for RFC 822 - rfc822msg = "From: #{@from}\n" - rfc822msg << "To: #{@to.join(",")}\n" - rfc822msg << "Subject: #{@subject}\n" - rfc822msg << "Date: #{Time.new.rfc822}\n" - rfc822msg << "Message-Id: <#{"%.8f" % Time.now.to_f}@#{@domain}>\n\n" - rfc822msg << str - - ### send email - smtp = Net::SMTP.new(@server, @port) - smtp.enable_starttls_auto if @tls - smtp.start(@domain, @acct, @passwd, @authtype) {|smtp| smtp.sendmail(rfc822msg, @from, @to)} - self - rescue StandardError, TimeoutError => err - self.level = :off - ::Logging.log_internal {'e-mail notifications have been disabled'} - ::Logging.log_internal(-2) {err} - end - -end # class Email + class Email < ::Logging::Appender + include Buffering + + attr_reader :authentication, :to, :port + attr_accessor :address, :domain, :from, :subject + attr_accessor :user_name, :password, :enable_starttls_auto + + # call-seq: + # Email.new( name, :from => 'me@example.com', :to => 'you@example.com', :subject => 'Whoops!' ) + # + # Create a new email appender that will buffer messages and then send them + # out in batches to the listed recipients. See the options below to + # configure how emails are sent through you mail server of choice. All the + # buffering options apply to the email appender. + # + # The following options are required: + # + # :from - The base filename to use when constructing new log + # filenames. + # :to - The list of email recipients either as an Array or a comma + # separated list. + # + # The following options are optional: + # + # :subject - The subject line for the email. + # :address - Allows you to use a remote mail server. Just change it + # from its default "localhost" setting. + # :port - On the off chance that your mail server doesn't run on + # port 25, you can change it. + # :domain - If you need to specify a HELO domain, you can do it here. + # :user_name - If your mail server requires authentication, set the user + # name in this setting. + # :password - If your mail server requires authentication, set the + # password in this setting. + # :authentication - If your mail server requires authentication, you need + # to specify the authentication type here. This is a + # symbol and one of :plain (will send the password in + # the clear), :login (will send password Base64 + # encoded) or :cram_md5 (combines a Challenge/Response + # mechanism to exchange information and a cryptographic + # Message Digest 5 algorithm to hash important + # information) + # :enable_starttls_auto - When set to true, detects if STARTTLS is + # enabled in your SMTP server and starts to use it. + # + # Example: + # + # Setup an email appender that will buffer messages for up to 1 minute, + # and only send messages for ERROR and FATAL messages. This example uses + # Google's SMTP server with authentication to send out messages. + # + # Logger.appenders.email( 'email', + # :from => "server@example.com", + # :to => "developers@example.com", + # :subject => "Application Error [#{%x(uname -n).strip}]", + # + # :address => "smtp.google.com", + # :port => 443, + # :domain => "google.com", + # :user_name => "example", + # :password => "12345", + # :authentication => :plain, + # :enable_starttls_auto => true, + # + # :auto_flushing => 200, # send an email after 200 messages have been buffered + # :flush_period => 60, # send an email after one minute + # :level => :error # only process log events that are "error" or "fatal" + # ) + # + def initialize( name, opts = {} ) + opts[:header] = false + super(name, opts) + + af = opts.getopt(:buffsize) || + opts.getopt(:buffer_size) || + 100 + configure_buffering({:auto_flushing => af}.merge(opts)) + + # get the SMTP parameters + self.from = opts.getopt :from + raise ArgumentError, 'Must specify from address' if @from.nil? + + self.to = opts.getopt :to + raise ArgumentError, 'Must specify recipients' if @to.empty? + + self.subject = opts.getopt :subject, "Message from #{$0}" + self.address = opts.getopt(:server) || opts.getopt(:address) || 'localhost' + self.port = opts.getopt(:port, 25) + self.domain = opts.getopt(:domain, ENV['HOSTNAME']) || 'localhost.localdomain' + self.user_name = opts.getopt(:acct) || opts.getopt(:user_name) + self.password = opts.getopt(:passwd) || opts.getopt(:password) + self.enable_starttls_auto = opts.getopt(:enable_starttls_auto, false) + self.authentication = opts.getopt(:authtype) || opts.getopt(:authentication) || :plain + end + + # Close the email appender. If the layout contains a foot, it will not be + # sent as an email. + # + def close( *args ) + super(false) + end + + # If your mail server requires authentication, you need to specify the + # authentication type here. This is a symbol and one of :plain (will send + # the password in the clear), :login (will send password Base64 encoded) + # or :cram_md5 (combines a Challenge/Response mechanism to exchange + # information and a cryptographic Message Digest 5 algorithm to hash + # important information) + # + def authentication=( val ) + @authentication = val.to_s.to_sym + end + + # On the off chance that your mail server doesn't run on port 25, you can + # change it. + # + def port=( val ) + @port = Integer(val) + end + + # The list of email recipients. This can either be an Array of recipients + # or a comma separated list. A single recipient is also valid. + # + # email.to = ['mike@example.com', 'tony@example.com'] + # email.to = 'alicia@example.com' + # email.to = 'bob@example.com, andy@example.com, john@example.com' + # + def to=( val ) + @to = val.respond_to?(:split) ? val.split(',') : Array(val) + end + + + private + + # This method is called by the buffering code when messages need to be + # sent out as an email. + # + def canonical_write( str ) + ### build a mail header for RFC 822 + rfc822msg = "From: #{@from}\n" + rfc822msg << "To: #{@to.join(",")}\n" + rfc822msg << "Subject: #{@subject}\n" + rfc822msg << "Date: #{Time.new.rfc822}\n" + rfc822msg << "Message-Id: <#{"%.8f" % Time.now.to_f}@#{@domain}>\n\n" + rfc822msg << str + + ### send email + smtp = Net::SMTP.new(@address, @port) + smtp.enable_starttls_auto if @enable_starttls_auto and smtp.respond_to? :enable_starttls_auto + smtp.start(@domain, @user_name, @password, @authentication) { |s| s.sendmail(rfc822msg, @from, @to) } + self + rescue StandardError, TimeoutError => err + self.level = :off + ::Logging.log_internal {'e-mail notifications have been disabled'} + ::Logging.log_internal(-2) {err} + end + + end # class Email end # module Logging::Appenders diff --git a/test/appenders/test_email.rb b/test/appenders/test_email.rb index 6cad2206..814df91e 100644 --- a/test/appenders/test_email.rb +++ b/test/appenders/test_email.rb @@ -14,14 +14,14 @@ def setup flexmock(Net::SMTP).new_instances do |m| m.should_receive(:start).at_least.once.with( - 'test.logging', 'test', 'test', :cram_md5, Proc).and_yield(m) + 'test.logging', 'test', 'test', :plain, Proc).and_yield(m) m.should_receive(:sendmail).at_least.once.with(String, 'me', ['you']) end @appender = Logging.appenders.email('email', 'from' => 'me', 'to' => 'you', :buffer_size => '3', :immediate_at => 'error, fatal', - :domain => 'test.logging', :acct => 'test', :passwd => 'test' + :domain => 'test.logging', :user_name => 'test', :password => 'test' ) @levels = Logging::LEVELS end @@ -43,36 +43,36 @@ def test_initialize assert_equal(100, appender.auto_flushing) assert_equal([], appender.instance_variable_get(:@immediate)) - assert_equal('localhost', appender.server) + assert_equal('localhost', appender.address) assert_equal(25, appender.port) domain = ENV['HOSTNAME'] || 'localhost.localdomain' assert_equal(domain, appender.domain) - assert_equal(nil, appender.acct) - assert_equal(:cram_md5, appender.authtype) - assert_equal("Message of #{$0}", appender.subject) + assert_equal(nil, appender.user_name) + assert_equal(:plain, appender.authentication) + assert_equal("Message from #{$0}", appender.subject) appender = Logging.appenders.email('email', 'from' => 'lbrinn@gmail.com', 'to' => 'everyone', :buffsize => '1000', :immediate_at => 'error, fatal', - :server => 'smtp.google.com', :port => '443', - :domain => 'google.com', :acct => 'lbrinn', - :passwd => '1234', :authtype => 'plain', :tls => true, + :address => 'smtp.google.com', :port => '443', + :domain => 'google.com', :user_name => 'lbrinn', + :password => '1234', :authentication => 'plain', :enable_starttls_auto => true, :subject => "I'm rich and you're not" ) assert_equal('lbrinn@gmail.com', appender.instance_variable_get(:@from)) assert_equal(['everyone'], appender.instance_variable_get(:@to)) assert_equal(1000, appender.auto_flushing) - assert_equal('1234', appender.instance_variable_get(:@passwd)) + assert_equal('1234', appender.password) assert_equal([nil, nil, nil, true, true], appender.instance_variable_get(:@immediate)) - assert_equal('smtp.google.com', appender.server) + assert_equal('smtp.google.com', appender.address) assert_equal(443, appender.port) assert_equal('google.com', appender.domain) - assert_equal('lbrinn', appender.acct) - assert_equal(:plain, appender.authtype) - assert(appender.tls) + assert_equal('lbrinn', appender.user_name) + assert_equal(:plain, appender.authentication) + assert(appender.enable_starttls_auto) assert_equal("I'm rich and you're not", appender.subject) appender = Logging.appenders.email('email',