aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve/pop3_server.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mauve/pop3_server.rb')
-rw-r--r--lib/mauve/pop3_server.rb164
1 files changed, 146 insertions, 18 deletions
diff --git a/lib/mauve/pop3_server.rb b/lib/mauve/pop3_server.rb
index 645fe0d..52f31d0 100644
--- a/lib/mauve/pop3_server.rb
+++ b/lib/mauve/pop3_server.rb
@@ -4,25 +4,41 @@ require 'digest/sha1'
module Mauve
#
- # API to control the web server
+ # The POP3 server, where messages can also be read.
#
class Pop3Server < MauveThread
include Singleton
attr_reader :port, :ip
-
+
+ # Initialize the server
+ #
+ # Default port is 1110
+ # Default IP is 0.0.0.0
+ #
def initialize
super
self.port = 1110
self.ip = "0.0.0.0"
end
+ #
+ # Set the port
+ #
+ # @param [Integer] pr
+ # @raise [ArgumentError] if the port is not sane
+ # ~
def port=(pr)
raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
@port = pr
end
+ #
+ # Set the IP address. Unfortunately IPv6 is not OK.
+ #
+ # @param [String] i The IP address required.
+ #
def ip=(i)
raise ArgumentError, "ip must be a string" unless i.is_a?(String)
#
@@ -32,22 +48,15 @@ module Mauve
@ip = i
end
-
+
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
- def main_loop
- unless @server and @server.running?
- @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
- logger.info "Listening on #{@server.to_s}"
- #
- # The next statment doesn't return.
- #
- @server.start
- end
- end
-
+ #
+ # This stops the server
+ #
def stop
if @server.running?
@server.stop
@@ -58,25 +67,50 @@ module Mauve
super
end
+ #
+ # This stops the server faster than stop
+ #
def join
@server.stop! if @server
super
end
+ private
+
+ #
+ # This tarts the server, and keeps it going.
+ #
+ def main_loop
+ unless @server and @server.running?
+ @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
+ logger.info "Listening on #{@server.to_s}"
+ #
+ # The next statment doesn't return.
+ #
+ @server.start
+ end
+ end
+
end
+ #
+ # This is the Pop3 Server itself. It is based on the Thin HTTP server, and hence EventMachine.
+ #
class Pop3Backend < Thin::Backends::TcpServer
+ #
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
- # Connect the server
+ # Initialize a new connection to the server
def connect
@signature = EventMachine.start_server(@host, @port, Pop3Connection)
end
+ # Disconnect the server, but only if EventMachine is still going.
def disconnect
#
# Only do this if EventMachine is still going.. The http_server may have
@@ -87,17 +121,25 @@ module Mauve
end
-
+ #
+ # This class represents and individual connection, and understands some POP3
+ # commands.
+ #
class Pop3Connection < EventMachine::Connection
+ # The username
attr_reader :user
+ # Default CR+LF combo.
CRLF = "\r\n"
+ # @return [Log4r::Logger]
def logger
@logger ||= Log4r::Logger.new(self.class.to_s)
end
+ # This is called once the connection has been established. It says hello
+ # to the client, and resets the state.
def post_init
logger.info "New connection"
send_data "+OK #{self.class.to_s} started"
@@ -107,6 +149,11 @@ module Mauve
@level = nil
end
+ # This returns a list of commands allowed in a state.
+ #
+ # @param [Symbol] The state to query, defaults to the current state.
+ # @return [Array] An array of permitted comands.
+ #
def permitted_commands(state=@state)
case @state
when :authorization
@@ -118,6 +165,10 @@ module Mauve
end
end
+ # This returns a list of capabilities in a given state.
+ #
+ # @param [Symbol] The state to query, defaults to the current state.
+ # @return [Array] An array of capabilities.
def capabilities(state=@state)
case @state
when :transaction
@@ -129,6 +180,27 @@ module Mauve
end
end
+ # This method handles a command, and parses it.
+ #
+ # The following POP3 commands are understood:
+ # QUIT
+ # USER
+ # PASS
+ # STAT
+ # LIST
+ # RETR
+ # DELE
+ # NOOP
+ # RSET
+ # CAPA
+ # UIDL
+ #
+ # The command is checked against a list of permitted commands, given the
+ # state of the connection, and returns an error if the command is
+ # forbidden.
+ #
+ # @param [String] data The data to process.
+ #
def receive_data (data)
data.split(CRLF).each do |cmd|
break if error?
@@ -165,16 +237,39 @@ module Mauve
end
end
end
-
+
+ # This sends the data back to the user. A CR+LF is joined to the end of
+ # the data.
+ #
+ # @param [String] d The data to send back.
def send_data(d)
d += CRLF
super unless error?
end
+ private
+
+ # This deals with CAPA, returning a string of capabilities in the current
+ # connection state.
+ #
+ # @param [String] a The complete CAPA command sent by the client.
+ #
def do_process_capa(a)
send_data (["+OK Capabilities follow:"] + self.capabilities + ["."]).join(CRLF)
end
+ # This deals with the USER command.
+ #
+ # Any of low, normal, urgent can be appended to the username, to select
+ # only alarms of that level to be shown.
+ #
+ # e.g.
+ # patrick+low
+ #
+ # will show only alerts of a LOW level.
+ #
+ # @param [String] s The complete USER command sent by the client.
+ #
def do_process_user(s)
allowed_levels = Mauve::AlertGroup::LEVELS.collect{|l| l.to_s}
@@ -195,6 +290,10 @@ module Mauve
end
end
+ # This processes the PASS command. It uses the Mauve::Authenticate class
+ # to authenticate the user. Once authenticated, the state is set to :transaction.
+ #
+ # @param [String] s The complete PASS command sent by the client.
def do_process_pass(s)
if @user and s =~ /\APASS +(\S+)/
@@ -209,16 +308,27 @@ module Mauve
end
end
+ #
+ # This just sends an "ERR Unknown command" string back to the user.
+ #
+ # @param [String] a The complete command from the client that caused this error.
def do_process_error(a)
send_data "-ERR Unknown comand."
end
+ # This does a NOOP.
+ #
+ # @param [String] a The complete NOOP command from the client.
def do_process_noop(a)
send_data "+OK Thanks."
end
+ # Delete is processed as a NOOP
alias do_process_dele do_process_noop
+ # This logs a user out, and closes the connection. The state is set to :update.
+ #
+ # @param [String] a The complete QUIT command from the client.
def do_process_quit(a)
@state = :update
@@ -227,10 +337,17 @@ module Mauve
close_connection_after_writing
end
+ # This sends the number of messages, and their size back to the client.
+ #
+ # @param [String] a The complete STAT command from the client.
def do_process_stat(a)
send_data "+OK #{self.messages.length} #{self.messages.inject(0){|s,m| s+= m[1].length}}"
end
+ # This sends a list of the messages back to the client.
+ #
+ # @param [String] a The complete LIST command from the client.
+ #
def do_process_list(a)
d = []
if a =~ /\ALIST +(\d+)\b/
@@ -249,6 +366,9 @@ module Mauve
send_data d.join(CRLF)
end
+ # This sends the UID of a message back to the client.
+ #
+ # @param [String] a The complete UIDL command from the client.
def do_process_uidl(a)
if a =~ /\AUIDL +(\d+)\b/
ind = $1.to_i
@@ -267,6 +387,10 @@ module Mauve
end
end
+ # This retrieves a message for the client.
+ #
+ # @param [String] a The complete RETR command from the client.
+ #
def do_process_retr(a)
if a =~ /\ARETR +(\d+)\b/
ind = $1.to_i
@@ -287,7 +411,11 @@ module Mauve
end
#
- # These are the messages in the mailbox.
+ # These are the messages in the mailbox. It looks for the first 100 alert_changed, and formats them into emails, and returns an array of
+ #
+ # [alert_changed, email]
+ #
+ # @return [Array] Array of alert_changeds and emails.
#
def messages
if @messages.empty?