diff options
Diffstat (limited to 'lib/mauve/pop3_server.rb')
-rw-r--r-- | lib/mauve/pop3_server.rb | 164 |
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? |