Skip to content

Commit

Permalink
Add ActionMailer compatible settings for the email appender.
Browse files Browse the repository at this point in the history
These changes add ActionMailer compatible settings for the email appender. The
documentation for the email appender has been significantly improved along
with an example for connecting to and sending messages via gmail.

closes #27
  • Loading branch information
TwP committed Feb 18, 2012
1 parent 8e9d5cb commit 85299b7
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 79 deletions.
225 changes: 160 additions & 65 deletions lib/logging/appenders/email.rb
Expand Up @@ -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

28 changes: 14 additions & 14 deletions test/appenders/test_email.rb
Expand Up @@ -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
Expand All @@ -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',
Expand Down

0 comments on commit 85299b7

Please sign in to comment.