diff options
Diffstat (limited to 'lib/mauve/pop3_server.rb')
-rw-r--r-- | lib/mauve/pop3_server.rb | 481 |
1 files changed, 0 insertions, 481 deletions
diff --git a/lib/mauve/pop3_server.rb b/lib/mauve/pop3_server.rb deleted file mode 100644 index 971adae..0000000 --- a/lib/mauve/pop3_server.rb +++ /dev/null @@ -1,481 +0,0 @@ -require 'thin' -require 'mauve/mauve_thread' -require 'digest/sha1' - -module Mauve - # - # 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) - # - # Use ipaddr to sanitize our IP. - # - IPAddr.new(i) - - @ip = i - end - - # @return [Log4r::Logger] - def logger - @logger ||= Log4r::Logger.new(self.class.to_s) - end - - # - # This stops the server - # - def stop - if @server.running? - @server.stop - else - @server.stop! - end - - super - end - - # - # This stops the server faster than stop - # - def join - @server.stop! if @server - - super - end - - # - # Since Server.start doesn't return below, we can't check when the thread was last polled. - # - def last_polled_at - Time.now - end - - private - - # - # This starts 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 - - # 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 - # stopped it already. - # - EventMachine.stop_server(@signature) if EventMachine.reactor_running? - end - - 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" - @state = :authorization - @user = nil - @messages = [] - @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 - %w(QUIT USER PASS CAPA) - when :transaction - %w(QUIT STAT LIST RETR DELE NOOP RSET UIDL CAPA) - when :update - %w(QUIT) - 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 - %w(CAPA UIDL) - when :authorization - %w(CAPA UIDL USER) - else - [] - 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? - - if cmd =~ Regexp.new('\A('+self.permitted_commands.join("|")+')\b') - case $1 - when "QUIT" - do_process_quit cmd - when "USER" - do_process_user cmd - when "PASS" - do_process_pass cmd - when "STAT" - do_process_stat cmd - when "LIST" - do_process_list cmd - when "RETR" - do_process_retr cmd - when "DELE" - do_process_dele cmd - when "NOOP" - do_process_noop cmd - when "RSET" - do_process_rset cmd - when "CAPA" - do_process_capa cmd - when "UIDL" - do_process_uidl cmd - else - do_process_error cmd - end - else - do_process_error cmd - 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} - - if s =~ /\AUSER +(\w+)\+(#{allowed_levels.join("|")})/ - # Allow alerts to be shown by level. - # - @user = $1 - @level = $2 - # - send_data "+OK Only going to show #{@level} alerts." - - elsif s =~ /\AUSER +([\w]+)/ - @user = $1 - - send_data "+OK" - else - send_data "-ERR Username not understood." - 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+)/ - if Mauve::Authentication.authenticate(@user, $1) - @state = :transaction - send_data "+OK Welcome #{@user} (#{@level})." - else - send_data "-ERR Authentication failed." - end - else - send_data "-ERR USER comes first." - 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 - - send_data "+OK bye." - - 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/ - ind = $1.to_i - if ind > 0 and ind <= self.messages.length - d << "+OK #{ind} #{self.messages[ind-1][1].length}" - else - d << "-ERR Unknown message." - end - else - d << "+OK #{self.messages.length} messages (#{self.messages.inject(0){|s,m| s+= m[1].length}} octets)." - self.messages.each_with_index{|m,i| d << "#{i+1} #{m[1].length}"} - d << "." - end - - 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 - if ind > 0 and ind <= self.messages.length - m = self.messages[ind-1][0].id - send_data "+OK #{ind} #{m}" - else - send_data "-ERR Message not found." - end - else - d = ["+OK "] - self.messages.each_with_index{|m,i| d << "#{i+1} #{m[0].id}"} - d << "." - - send_data d.join(CRLF) - 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 - if ind > 0 and ind <= self.messages.length - alert_changed, msg = self.messages[ind-1] - send_data ["+OK #{msg.length} octets", msg, "."].join(CRLF) - note = "#{alert_changed.update_type.capitalize} notification downloaded via POP3 by #{@user}" - logger.info note+" about #{alert_changed}." - h = History.new(:alerts => [alert_changed.alert_id], :type => "notification", :event => note, :user => @user) - logger.error "Unable to save history due to #{h.errors.inspect}" if !h.save - else - send_data "-ERR Message not found." - end - else - send_data "-ERR Boo." - end - - end - - protected - - # - # 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? - @messages = [] - - email = Configuration.current.notification_methods['email'] - - alerts_seen = [] - - # - # A maximum of the 100 most recent alerts. - # - AlertChanged.first(100, :person => self.user, :was_relevant => true).each do |a| - # - # Not interested in alerts - # - next unless @level.nil? or a.level.to_s == @level - - # - # Only interested in alerts - # - next unless a.alert.is_a?(Mauve::Alert) - - # - # Only one message per alert. - # - next if alerts_seen.include?([a.alert_id, a.update_type]) - - relevant = case a.update_type - when "raised" - a.alert.raised? - when "acknowledged" - a.alert.acknowledged? - when "cleared" - a.alert.cleared? - else - false - end - - next unless relevant - - alerts_seen << [a.alert_id, a.update_type] - - @messages << [a, email.prepare_message(self.user+"@"+Server.instance.hostname, a.alert, [])] - end - end - - @messages - end - - end - -end - |