# POPFILE LOADABLE MODULE
package Proxy::POP3;

use Proxy::Proxy;
use Digest::MD5;
@ISA = ("Proxy::Proxy");

# ----------------------------------------------------------------------------
#
# This module handles proxying the POP3 protocol for POPFile.
#
# Copyright (c) 2001-2006 John Graham-Cumming
#
#   This file is part of POPFile
#
#   POPFile is free software; you can redistribute it and/or modify it
#   under the terms of version 2 of the GNU General Public License as
#   published by the Free Software Foundation.
#
#   POPFile is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with POPFile; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#   Modified by     Sam Schinke (sschinke@users.sourceforge.net)
#
# ----------------------------------------------------------------------------

use strict;
use warnings;
use locale;

# A handy variable containing the value of an EOL for networks
my $eol = "\015\012";

#----------------------------------------------------------------------------
# new
#
#   Class new() function
#----------------------------------------------------------------------------
sub new
{
    my $type = shift;
    my $self = Proxy::Proxy->new();

    # Must call bless before attempting to call any methods

    bless $self, $type;

    $self->name( 'pop3' );

    $self->{child_} = \&child__;
    $self->{connection_timeout_error_} = '-ERR no response from mail server';
    $self->{connection_failed_error_}  = '-ERR can\'t connect to';
    $self->{good_response_}            = '^\+OK';

    # Client requested APOP
    $self->{use_apop__} = 0;

    # APOP username
    $self->{apop_user__} = '';

    # The APOP portion of the banner sent by the POP3 server
    $self->{apop_banner__} = undef;

    return $self;
}

# ----------------------------------------------------------------------------
#
# initialize
#
# Called to initialize the POP3 proxy module
#
# ----------------------------------------------------------------------------
sub initialize
{
    my ( $self ) = @_;

    # Enabled by default
    $self->config_( 'enabled', 1);

    # By default we don't fork on Windows
    $self->config_( 'force_fork', ($^O eq 'MSWin32')?0:1 );

    # Default ports for POP3 service and the user interface
    $self->config_( 'port', 110 );

    # There is no default setting for the secure server
    $self->config_( 'secure_server', '' );
    $self->config_( 'secure_port', 110 );

    # Only accept connections from the local machine for POP3
    $self->config_( 'local', 1 );

    # Whether to do classification on TOP as well
    $self->config_( 'toptoo', 0 );

    # The separator within the POP3 username is :
    $self->config_( 'separator', ':' );

    # The welcome string from the proxy is configurable
    $self->config_( 'welcome_string',
        "POP3 POPFile ($self->{version_}) server ready" );

    return $self->SUPER::initialize();
}

# ----------------------------------------------------------------------------
#
# start
#
# ----------------------------------------------------------------------------
sub start
{
    my ( $self ) = @_;

    # If we are not enabled then no further work happens in this module

    if ( $self->config_( 'enabled' ) == 0 ) {
        return 2;
    }

    # Tell the user interface module that we having a configuration
    # item that needs a UI component

    $self->register_configuration_item_( 'configuration',              # PROFILE BLOCK START
                                         'pop3_configuration',
                                         'pop3-configuration-panel.thtml',
                                         $self );                      # PROFILE BLOCK STOP

    $self->register_configuration_item_( 'security',                   # PROFILE BLOCK START
                                         'pop3_security',
                                         'pop3-security-panel.thtml',
                                         $self );                      # PROFILE BLOCK STOP

    $self->register_configuration_item_( 'chain',                      # PROFILE BLOCK START
                                         'pop3_chain',
                                         'pop3-chain-panel.thtml',
                                         $self );                      # PROFILE BLOCK STOP

    if ( $self->config_( 'welcome_string' ) =~ /^POP3 POPFile \(v\d+\.\d+\.\d+\) server ready$/ ) { # PROFILE BLOCK START
        $self->config_( 'welcome_string', "POP3 POPFile ($self->{version_}) server ready" );        # PROFILE BLOCK STOP
    }

    return $self->SUPER::start();
}

# ----------------------------------------------------------------------------
#
# child__
#
# The worker method that is called when we get a good connection from a client
#
# $client         - an open stream to a POP3 client
# $session        - API session key
#
# ----------------------------------------------------------------------------
sub child__
{
    my ( $self, $client, $session ) = @_;

    # Hash of indexes of downloaded messages mapped to their
    # slot IDs

    my %downloaded;

    # The handle to the real mail server gets stored here

    my $mail;

    $self->{apop_banner__} = undef;
    $self->{use_apop__} = 0;
    $self->{apop_user__} = '';

    # Tell the client that we are ready for commands and identify our
    # version number

    $self->tee_( $client, "+OK " . $self->config_( 'welcome_string' ) . "$eol" );

    # Compile some configurable regexp's once

    my $s = $self->config_( 'separator' );
    $s =~ s/(\$|\@|\[|\]|\(|\)|\||\?|\*|\.|\^|\+)/\\$1/;

    my $transparent  = "^USER ([^$s])+\$";
    my $user_command = "USER ([^$s]+)($s(\\d+))?$s([^$s]+)($s([^$s]+))?";
    my $apop_command = "APOP ([^$s]+)($s(\\d+))?$s([^$s]+) (.*?)";

    $self->log_( 2, "Regexps: $transparent, $user_command, $apop_command" );

    # Retrieve commands from the client and process them until the
    # client disconnects or we get a specific QUIT command

    while  ( <$client> ) {
        my $command;

        $command = $_;

        # Clean up the command so that it has a nice clean $eol at the
        # end

        $command =~ s/(\015|\012)//g;

        $self->log_( 2, "Command: --$command--" );

        # The USER command is a special case because we modify the
        # syntax of POP3 a little to expect that the username being
        # passed is actually of the form host:username where host is
        # the actual remote mail server to contact and username is the
        # username to pass through to that server and represents the
        # account on the remote machine that we will pull email from.
        # Doing this means we can act as a proxy for multiple mail
        # clients and mail accounts
        #
        # When the client issues the command "USER host:username:apop"
        # POPFile must acknowledge the command and be prepared to
        # compute the md5 digest of the user's password and the real
        # pop server's banner upon receipt of a PASS command.
        #
        # When the client issues the command "USER host:username:ssl"
        # POPFile will use SSL for the connection to the remote, note
        # that the user can say host:username:ssl,apop if both are
        # needed

        if ( $command =~ /$transparent/ ) {
            if ( $self->config_( 'secure_server' ) ne '' )  {
                if ( $mail = $self->verify_connected_( $mail, $client,  $self->config_( 'secure_server' ), $self->config_( 'secure_port' ) ) )  {
                    last if ($self->echo_response_($mail, $client, $command) == 2 );
                } else {
                    next;
                }
            } else {
                $self->tee_(  $client, "-ERR Transparent proxying not configured: set secure server/port$eol" );
            }

            next;
        }

        if ( $command =~ /$user_command/i ) {
            if ( $1 ne '' )  {
                my ( $host, $port, $user, $options ) = ($1, $3, $4, $6);

                $self->mq_post_( 'LOGIN', $user );

                my $ssl = defined( $options ) && ( $options =~ /ssl/i );
                $port = $ssl?995:110 if ( !defined( $port ) );

                if ( $mail = $self->verify_connected_( $mail, $client,
                                 $host, $port, $ssl ) )  {

                    if ( defined( $options ) && ( $options =~ /apop/i ) ) {

                        # We want to make sure the server sent a real
                        # APOP banner, containing <>'s

                        $self->{apop_banner__} = $1 if $self->{connect_banner__} =~ /(<[^>]+>)/;
                        $self->log_( 2, "banner=" . $self->{apop_banner__} ) if defined( $self->{apop_banner__} );

                        # any apop banner is ok

                        if ( defined($self->{apop_banner__})) {
                            $self->{use_apop__} = 1; #
                            $self->log_( 2, "auth APOP" );
                            $self->{apop_user__} = $user;

                            # tell the client that username was
                            # accepted don't flush_extra, we didn't
                            # send anything to the real server

                            $self->tee_( $client, "+OK hello $user$eol" );
                            next;
                        } else {

                            # If the client asked for APOP, and the
                            # server doesn't have the correct banner,
                            # give a meaningful error instead of
                            # whatever error the server might have if
                            # we try to make up a hash

                            $self->{use_apop__} = 0;
                            $self->tee_( $client, "-ERR $host doesn't support APOP, aborting authentication$eol" );
                            next;
                        }
                    } else {

                        # Pass through the USER command with the
                        # actual user name for this server, and send
                        # the reply straight to the client

                        $self->log_( 2, "auth plaintext" );
                        $self->{use_apop__} = 0;         # signifies a non-apop connection
                        last if ($self->echo_response_( $mail, $client, 'USER ' . $user ) == 2 );
                    }

                } else {

                    # If the login fails then we want to continue in
                    # the unlogged in state so that clients can send
                    # us the QUIT command

                    next;
                }
            }

            next;
        }

        # User is issuing the APOP command to start a session with the
        # remote server

        if ( ( $command =~ /PASS (.*)/i ) ) {
            if ( $self->{use_apop__} ) {

                # Authenticate with APOP

                my $md5 = Digest::MD5->new;

                $md5->add( $self->{apop_banner__}, $1 );
                my $md5hex = $md5->hexdigest;
                $self->log_( 2, "digest='$md5hex'" );

                my ($response, $ok) =
                    $self->get_response_( $mail, $client,
                        "APOP $self->{apop_user__} $md5hex", 0, 1 );
                if ( ( $ok == 1 ) &&
                     ( $response =~ /$self->{good_response_}/ ) ) {

                    # authentication OK, toss the hello response and
                    # return password ok

                    $self->tee_( $client, "+OK password ok$eol" );
                } else {
                    $self->tee_( $client, "$response" );
                }
             } else {
               last if ($self->echo_response_($mail, $client, $command) == 2 );
             }
             next;
        }

        # User is issuing the APOP command to start a session with the
        # remote server We'd need a copy of the plaintext password to
        # support this.

        if ( $command =~ /$apop_command/io ) {
            $self->tee_( $client,
              "-ERR APOP not supported between mail client and POPFile.$eol" );

            # TODO: Consider implementing a host:port:username:secret
            # hash syntax for proxying the APOP command

            next;
        }

        # Secure authentication

        if ( $command =~ /AUTH ([^ ]+)/ ) {
            if ( $self->config_( 'secure_server' ) ne '' )  {
                if ( $mail = $self->verify_connected_( $mail, $client,  $self->config_( 'secure_server' ), $self->config_( 'secure_port' ) ) )  {

                    # Loop until we get -ERR or +OK

                    my ( $response, $ok ) = $self->get_response_( $mail, $client, $command );

                    while ( ( ! ( $response =~ /\+OK/ ) ) && ( ! ( $response =~ /-ERR/ ) ) ) {
                        my $auth;
                        $auth = <$client>;
                        $auth =~ s/(\015|\012)$//g;
                        ( $response, $ok ) = $self->get_response_( $mail, $client, $auth );
                    }
                } else {
                    next;
                }
            } else {
                $self->tee_(  $client, "-ERR No secure server specified$eol" );
            }

            next;
        }

        if ( $command =~ /AUTH/ ) {
            if ( $self->config_( 'secure_server' ) ne '' )  {
                if ( $mail = $self->verify_connected_( $mail, $client,  $self->config_( 'secure_server' ), $self->config_( 'secure_port' ) ) )  {
                    my $response = $self->echo_response_($mail, $client, "AUTH" );
                    last if ( $response == 2 );
                    if ( $response == 0 ) {
                        $self->echo_to_dot_( $mail, $client );
                    }
                } else {
                    next;
                }
            } else {
                $self->tee_(  $client, "-ERR No secure server specified$eol" );
            }

            next;
        }

        # The client is requesting a LIST/UIDL of the messages

        if ( ( $command =~ /LIST ?(.*)?/i ) ||       # PROFILE BLOCK START
             ( $command =~ /UIDL ?(.*)?/i ) ) {      # PROFILE BLOCK STOP
            my $response = $self->echo_response_($mail, $client, $command );
            last if ( $response == 2 );
            if ( $response == 0 ) {
                $self->echo_to_dot_( $mail, $client ) if ( $1 eq '' );
            }

            next;
        }

        # TOP handling is rather special because we have three cases
        # that we handle
        #
        # 1. If the client sends TOP x 99999999 then it is most likely
        #    to be fetchmail and the intent of fetchmail is to
        #    actually get the message but for its own reasons it does
        #    not use RETR.  We use RETR as the clue to place a message
        #    in the history, so we have a hack.  If the client looks
        #    like fetchmail then TOP x 99999999 is actually
        #    implemented using RETR
        #
        # 2. The toptoo configuration controls whether email
        #    downloaded using the TOP command is classified or not (It
        #    may be downloaded and cached for bandwidth efficiency, and
        #    thus appear in the history).  There are two cases:
        #
        # 2a If toptoo is 0 then POPFile will pass a TOP from the
        #    client through as a TOP and do no classification on the
        #    message.
        #
        # 2b If toptoo is 1 then POPFile first does a RETR on the
        #    message and saves it in the history so that it can get the
        #    classification on the message which is stores in $class.
        #    Then it gets the message again by sending the TOP command
        #    and passing the result through classify_and_modify passing
        #    in the $class determined above.  This means that the message
        #    gets the right classification and the client only gets the
        #    headers requested plus so many lines of body, but they will
        #    get subject modification, and the XTC and XPL headers add.
        #    Note that TOP always returns the full headers and then n
        #    lines of the body so we are guaranteed to be able to do our
        #    header modifications.
        #
        # NOTE messages retrieved using TOPTOO are visible in the
        #      history as they are "cached" to avoid requiring repeated
        #      downloads if the client issues a RETR for the message in
        #      the same session
        #
        # NOTE using toptoo=1 on a slow link could cause
        #      performance problems, in cases where only the headers,
        #      but not classification, is required.  toptoo=1 is,
        #      however, appropriate for normal use via a mail client and
        #      won't significantly increase bandwidth unless the mail
        #      client is selectively downloading messages based on
        #      non-classification data in the TOP headers.

        if ( $command =~ /TOP (.*) (.*)/i ) {
            my $count = $1;

            if ( $2 ne '99999999' )  {
                if ( $self->config_( 'toptoo' ) == 1 ) {
                    my $response =
                        $self->echo_response_( $mail, $client, "RETR $count" );
                    last if ( $response == 2 );
                    if ( $response == 0 ) {

                        # Classify without echoing to client, saving
                        # file for later RETR's

                        my ( $class, $slot ) =
                             $self->{classifier__}->classify_and_modify(
                                 $session, $mail, $client, 0, '', 0, 0 );

                        $downloaded{$count} = $slot;

                        # Note that the 1 here indicates that
                        # echo_response_ does not send the response to
                        # the client.  The +OK has already been sent
                        # by the RETR

                        $response =
                            $self->echo_response_( $mail, $client,
                                $command, 1 );
                        last if ( $response == 2 );
                        if ( $response == 0 ) {

                            # Classify with pre-defined class, without
                            # saving, echoing to client

                            $self->{classifier__}->classify_and_modify(
                                $session, $mail, $client, 1, $class, $slot, 1 );
                        }
                    }
                } else {
                    my $response =
                        $self->echo_response_( $mail, $client, $command );
                    last if ( $response == 2 );
                    if ( $response == 0 ) {
                        $self->echo_to_dot_( $mail, $client );
		    }
                }

                next;
            }

            # Note the fall through here.  Later down the page we look
            # for TOP x 99999999 and do a RETR instead
        }

        # The CAPA command

        if ( $command =~ /CAPA/i ) {
            if ( $mail || $self->config_( 'secure_server' ) ne '' )  {
                if ( $mail || ( $mail = $self->verify_connected_( $mail, $client, $self->config_( 'secure_server' ), $self->config_( 'secure_port' ) ) ) )  {
                    my $response = $self->echo_response_($mail, $client, "CAPA" );
                    last if ( $response == 2 );
                    if ( $response == 0 ) {
                        $self->echo_to_dot_( $mail, $client );
		    }
                } else {
                    next;
                }
            } else {
                $self->tee_(  $client, "-ERR No secure server specified$eol" );
            }

            next;
        }

        # The HELO command results in a very simple response from us.
        # We just echo that we are ready for commands

        if ( $command =~ /HELO/i ) {
            $self->tee_(  $client, "+OK HELO POPFile Server Ready$eol" );
            next;
        }

        # In the case of PASS, NOOP, XSENDER, STAT, DELE and RSET
        # commands we simply pass it through to the real mail server
        # for processing and echo the response back to the client

        if ( ( $command =~ /NOOP/i )         ||                 # PROFILE BLOCK START
             ( $command =~ /STAT/i )         ||
             ( $command =~ /XSENDER (.*)/i ) ||
             ( $command =~ /DELE (.*)/i )    ||
             ( $command =~ /RSET/i ) ) {                        # PROFILE BLOCK STOP
            last if ( $self->echo_response_($mail, $client, $command ) == 2 );
            next;
        }

        # The client is requesting a specific message.  Note the
        # horrible hack here where we detect a command of the form TOP
        # x 99999999 this is done so that fetchmail can be used with
        # POPFile.

        if ( ( $command =~ /RETR (.*)/i ) || ( $command =~ /TOP (.*) 99999999/i ) )  {
            my $count = $1;
            my $class;
            my $file;

            if ( defined($downloaded{$count}) &&
                 ( $file = $self->{history__}->get_slot_file( $downloaded{$count} ) ) &&
                 (open RETRFILE, "<$file") ) {

                # act like a network stream

                binmode RETRFILE;

                # File has been fetched and classified already

                $self->log_( 1, "Printing message from cache" );

                # Give the client an +OK:

                $self->tee_( $client, "+OK " . ( -s $file ) . " bytes from POPFile cache$eol" );

                # Load the last classification

                my ( $id, $from, $to, $cc, $subject,
                    $date, $hash, $inserted, $bucket, $reclassified ) =
                    $self->{history__}->get_slot_fields( $downloaded{$count} );

                if ( $bucket ne 'unknown class' ) {

                    # echo file, inserting known classification,
                    # without saving

                    ($class, undef) = $self->{classifier__}->classify_and_modify( $session, \*RETRFILE, $client, 1, $bucket, $downloaded{$count} );
                    print $client ".$eol";

                } else {

                    # If the class wasn't saved properly, classify
                    # from disk normally

                    ($class, undef) = $self->{classifier__}->classify_and_modify( $session, \*RETRFILE, $client, 1, '', 0 );
                    print $client ".$eol";
                }

                close RETRFILE;
            } else {

                # Retrieve file directly from the server

                # Get the message from the remote server, if there's
                # an error then we're done, but if not then we echo
                # each line of the message until we hit the . at the
                # end

                my $response = $self->echo_response_($mail, $client, $command );

                last if ( $response == 2 );
                if ( $response == 0 ) {
                    my $slot;
                    ( $class, $slot ) = $self->{classifier__}->classify_and_modify( $session, $mail, $client, 0, '', 0 );

                    # Note locally that file has been retrieved if the
                    # full thing has been saved to disk

                    $downloaded{$count} = $slot;
                }
            }

            next;
        }

        # The mail client wants to stop using the server, so send that
        # message through to the real mail server, echo the response
        # back up to the client and exit the while.  We will close the
        # connection immediately

        if ( $command =~ /QUIT/i ) {
            if ( $mail )  {
                last if ( $self->echo_response_( $mail, $client, $command ) == 2 );
                close $mail;
            } else {
                $self->tee_( $client, "+OK goodbye$eol" );
            }
            last;
        }

        # Don't know what this is so let's just pass it through and
        # hope for the best

        if ( $mail && $mail->connected )  {
            last if ( $self->echo_response_($mail, $client, $command ) == 2 );
            next;
        } else {
            $self->tee_(  $client, "-ERR unknown command or bad syntax$eol" );
            next;
        }

    }

    if ( defined( $mail ) ) {
        $self->done_slurp_( $mail );
        close $mail;
    }

    close $client;
    $self->mq_post_( 'CMPLT', $$ );
    $self->log_( 0, "POP3 proxy done" );
}

# ----------------------------------------------------------------------------
#
# configure_item
#
#    $name            Name of this item
#    $templ           The loaded template that was passed as a parameter
#                     when registering
#    $language        Current language
#
# ----------------------------------------------------------------------------

sub configure_item
{
    my ( $self, $name, $templ, $language ) = @_;

    if ( $name eq 'pop3_configuration' ) {
        $templ->param( 'POP3_Configuration_If_Force_Fork' => ( $self->config_( 'force_fork' ) == 0 ) );
        $templ->param( 'POP3_Configuration_Port'          => $self->config_( 'port' ) );
        $templ->param( 'POP3_Configuration_Separator'     => $self->config_( 'separator' ) );
    } else {
        if ( $name eq 'pop3_security' ) {
            $templ->param( 'POP3_Security_Local' => ( $self->config_( 'local' ) == 1 ) );
	} else {
            if ( $name eq 'pop3_chain' ) {
                $templ->param( 'POP3_Chain_Secure_Server' => $self->config_( 'secure_server' ) );
                $templ->param( 'POP3_Chain_Secure_Port' => $self->config_( 'secure_port' ) );
	    } else {
                $self->SUPER::configure_item( $name, $templ, $language );
	    }
        }
    }
}

# ----------------------------------------------------------------------------
#
# validate_item
#
#    $name            The name of the item being configured, was passed in by the call
#                     to register_configuration_item
#    $templ           The loaded template
#    $language        The language currently in use
#    $form            Hash containing all form items
#
# ----------------------------------------------------------------------------

sub validate_item
{
    my ( $self, $name, $templ, $language, $form ) = @_;

    if ( $name eq 'pop3_configuration' ) {
        if ( defined($$form{pop3_port}) ) {
            if ( ( $$form{pop3_port} >= 1 ) && ( $$form{pop3_port} < 65536 ) ) {
                $self->config_( 'port', $$form{pop3_port} );
                $templ->param( 'POP3_Configuration_If_Port_Updated' => 1 );
                $templ->param( 'POP3_Configuration_Port_Updated' => sprintf( $$language{Configuration_POP3Update}, $self->config_( 'port' ) ) );
            } else {
                $templ->param( 'POP3_Configuration_If_Port_Error' => 1 );
            }
        }

        if ( defined($$form{pop3_separator}) ) {
            if ( length($$form{pop3_separator}) == 1 ) {
                $self->config_( 'separator', $$form{pop3_separator} );
                $templ->param( 'POP3_Configuration_If_Sep_Updated' => 1 );
                $templ->param( 'POP3_Configuration_Sep_Updated' => sprintf( $$language{Configuration_POP3SepUpdate}, $self->config_( 'separator' ) ) );
            } else {
                $templ->param( 'POP3_Configuration_If_Sep_Error' => 1 );
            }
        }

        if ( defined($$form{pop3_force_fork}) ) {
            $self->config_( 'force_fork', $$form{pop3_force_fork} );
        }

        return;
    }

    if ( $name eq 'pop3_security' ) {
        $self->config_( 'local', $$form{pop3_local}-1 ) if ( defined($$form{pop3_local}) );

        return;
    }

    if ( $name eq 'pop3_chain' ) {
        if ( defined( $$form{server} ) ) {
            $self->config_( 'secure_server', $$form{server} );
            $templ->param( 'POP3_Chain_If_Server_Updated' => 1 );
            $templ->param( 'POP3_Chain_Server_Updated' => sprintf( $$language{Security_SecureServerUpdate}, $self->config_( 'secure_server' ) ) );
	}

        if ( defined($$form{sport}) ) {
            if ( ( $$form{sport} >= 1 ) && ( $$form{sport} < 65536 ) ) {
                $self->config_( 'secure_port', $$form{sport} );
                $templ->param( 'POP3_Chain_If_Port_Updated' => 1 );
                $templ->param( 'POP3_Chain_Port_Updated' => sprintf( $$language{Security_SecurePortUpdate}, $self->config_( 'secure_port' ) ) );
            } else {
                $templ->param( 'POP3_Chain_If_Port_Error' => 1 );
            }
        }

        return;
    }

    $self->SUPER::validate_item( $name, $templ, $language, $form );
}

