The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Mail::Decency::Core::POEForking::SMTP;

use strict;
use warnings;

use version 0.74; our $VERSION = qv( "v0.1.5" );

use feature 'switch';

use base qw/
    Mail::Decency::Core::POEForking
/;

use POE qw/
    Wheel::ReadWrite
/;

use File::Temp qw/ tempfile /;
use Mail::Decency::ContentFilter::Core::Constants;
use Mail::Decency::Core::POEFilterSMTP;
use Socket qw/ inet_ntoa /;
use Data::Dumper;

=head1 NAME

Mail::Decency::Core::POEForking::SMTP

=head1 DESCRIPTION

SMTP Server for the content filter

=head1 METHODS

=head2 create_handler

Called by the forking/treading parent server

=cut

sub create_handler {
    my $class = shift;
    my ( $heap, $session, $socket, $peer_addr, $peer_port )
        = @_[ HEAP, SESSION, ARG0, ARG1, ARG2 ];
    
    POE::Session->create(
        inline_states => {
            _start     => \&smtp_start,
            _stop      => \&smtp_stop,
            smtp_input => \&smtp_input,
            smtp_flush => \&smtp_flush,
            smtp_error => \&smtp_error,
            good_night => \&smtp_stop,
            _parent    => sub { 0 },
            # _default        => sub {
            #     my ($event, $args) = @_[ARG0, ARG1];
            #     warn "** SMTP: UNKNOWN EVENT $event, $args\n";
            # }
        },
        heap => {
            decency   => $heap->{ decency },
            conf      => $heap->{ conf },
            handler   => $heap->{ decency }->get_handlers(),
            logger    => $heap->{ decency }->logger,
            args      => $heap->{ args },
            parent    => $session,
            socket    => $socket,
            peer_addr => inet_ntoa( $peer_addr ),
            peer_port => $peer_port,
        }
    );
}


=head2 smtp_start

Start connection from postfix

=cut

sub smtp_start {
    my ( $kernel, $session, $heap ) = @_[ KERNEL, SESSION, HEAP ];
    
    # tell the parent session we are here
    $kernel->post( $heap->{ parent }, "new_client_session", $session );
    
    # start new r/w on the socket
    $heap->{ client } = POE::Wheel::ReadWrite->new(
        Handle       => $heap->{ socket },
        Filter       => Mail::Decency::Core::POEFilterSMTP->new(),
        InputEvent   => "smtp_input",
        ErrorEvent   => "smtp_error",
        FlushedEvent => "smtp_flush",
    );
    
    # say hello
    $heap->{ client }->put( 220 => "Welcome" );
}


=head2 smtp_input

Handling input

=cut

sub smtp_input {
    my ( $kernel, $heap, $session, $input ) = @_[ KERNEL, HEAP, SESSION, ARG0 ];
    return unless $input && ref( $input ) eq 'ARRAY';
    
    # first pass parse
    my ( $cmd, $arg, $line ) = @$input;
    $arg ||= "";
    
    # being in the DATA part:
    if ( $heap->{ in_data } ) {
        
        # last final "." -> end of DATA
        if ( $cmd eq '.' ) {
            
            # close file handle
            close delete $heap->{ mime_fh };
            
            # unmark data
            $heap->{ in_data } = 0;
            
            # handle input data with decency
            my ( $ok, $reject_message ) = $heap->{ handler }->( {
                file => delete $heap->{ mime_file },
                from => delete $heap->{ mail_from },
                to   => delete $heap->{ rcpt_to },
            } );
            
            # send bye to client
            if ( $ok ) {
                $heap->{ decency }->logger->debug2( "Send 250 to postfix, mail accepted" );
                $heap->{ client }->put( 250 => 'Bye ' );
            }
            else {
                $heap->{ decency }->logger->debug2( "Send 554 to postfix, mail bounced" );
                $heap->{ client }->put( 554 => $reject_message || "Rejected" );
            }
            
            # close connection to postfix
            delete $heap->{ $_ }
                for qw/ mail_from rcpt_to mime_fh mime_file /; # client socket
            
            
            # back to begin
            $heap->{ in_data } = 0;
        }
        
        # collecting DATA
        else {
            my $fh = $heap->{ mime_fh };
            print $fh $line;
        }
    }
    
    # not in DATA -> catch SMTP commands
    else {
        
        warn "> $cmd | $arg\n" if $ENV{ DEBUG_SMTP }; 
        
        # MAIL FROM commmand -> rewrite for handling
        if ( $cmd eq 'MAIL' && $arg =~ /^FROM:\s*(?:<([^>]*?)>|(.*?))\s*$/ ) {
            $cmd = 'MAIL_FROM';
            $heap->{ mail_from } = $arg = $1 || $2 || "";
        }
        
        # RCPT TO commmand -> rewrite for handling
        elsif ( $cmd eq 'RCPT' && $arg =~ /^TO:\s*(?:<([^>]*?)>|(.*?))\s*$/ ) {
            $cmd = 'RCPT_TO';
            $heap->{ rcpt_to } = $arg = $1 || $2;
        }
        
        # DATA commmand -> init data input
        elsif ( $cmd eq 'DATA' ) {
            
            # not heaving from and to ? not good -> bye to client
            unless ( $heap->{ rcpt_to } ) {
                
                # clear heap
                delete $heap->{ $_ } for qw/ mail_from /;
                
                # send bye, close client
                $heap->{ client }->put( 221 => 'Require RCPT TO' );
                
                return;
            }
            
            # mark being in data
            $heap->{ in_data } ++;
            
            # new temp file in spool dir
            my ( $th, $tn ) = tempfile( $heap->{ args }->{ temp_mask }, UNLINK => 0 );
            $heap->{ mime_fh }   = $th;
            $heap->{ mime_file } = $tn;
        }
        
        # RSET .. not out fault ;)
        elsif ( $cmd eq 'RSET' ) {
            $heap->{ in_data } = 0;
            close delete $heap->{ mime_fh }
                if $heap->{ mime_fh };
            delete $heap->{ $_ } for qw/ mime_file rcpt_to mail_from /; 
        }
        
        
        smtp_response( $heap, $session, $cmd, $line );
    }
}


=head2 smtp_stop

Stop connection, good bye

=cut

sub smtp_stop {
    my ( $heap, $session ) = @_[ HEAP, SESSION ];
    
    $heap->{ logger }->debug2( "Disconnecting from postfix" );
    eval {
        delete $heap->{ $_ } for qw/ client socket /;
    };
    $heap->{ logger }->error( "Could not remove client from list after postfix stop: $@" )
        if $@;
    #delete $heap->{ client } if $heap->{ client };
}


=head2 smtp_response

Handle a SMTP input (eg RCPT TO)

=cut

sub smtp_response {
    my ( $heap, $session, $cmd, $line ) = @_;
    
    given ( $cmd ) {
        when ( "EHLO" ) {
            $heap->{ client }->put( 250 => 'XFORWARD NAME ADDR PROTO HELO', 'OK' );
        }
        when ( "HELO" ) {
            $heap->{ client }->put( 250 => 'XFORWARD NAME ADDR PROTO HELO', 'OK' );
        }
        when ( "MAIL_FROM" ) {
            $heap->{ client }->put( 250 => 'OK' );
        }
        when ( "RCPT_TO" ) {
            $heap->{ client }->put( 250 => 'OK' );
        }
        when ( "DATA" ) {
            $heap->{ client }->put( 354 => 'End with "." on a line by itself' );
        }
        when ( "QUIT" ) {
            $heap->{ client }->put( 221 => 'Thanks for the fish' );
            $heap->{ finished } ++;
        }
        when ( "RSET" ) {
            $heap->{ client }->put( 250 => 'It\'s not my fault!' );
            $heap->{ finished } ++;
        }
        default {
            $heap->{ client }->put( 502 => 'This is not your postfix' );
        }
    }
}


=head2 smtp_flush

=cut

sub smtp_flush {
    my $heap = $_[ HEAP ];
    
    if ( delete $heap->{ finished } && $heap->{ client } ) {
        $heap->{ decency }->logger->debug2( "Got final flush from postfix. Delete client connection." );
        eval { delete $heap->{ client }; };
        $heap->{ decency }->logger->error( "Could not remove client from list after final flush: $@" ) if $@;
        
        # eval { $heap->{ socket }->flush; };
        # $heap->{ decency }->logger->error( "Could not flush socket after client removal: $@" ) if $@;
    }
}



=head2 smtp_error

Called uppon client error .. eg sudden disconnect

=cut

sub smtp_error {
    my ( $heap, $session, $operation, $errnum, $errstr, $id ) = @_[ HEAP, SESSION, ARG0..ARG3 ];
    
    # disconnect ..
    if ( $operation eq 'read' && $errnum == 0 ) {
        $heap->{ decency }->logger->debug2( "Postfix closed connection" );
    }
    
    # real error ..
    else {
        $heap->{ decency }->logger->error( "Weird disconnection from postfix ". $session->ID. " (OP: $operation, ENUM: $errnum, ESTR: $errstr, WID: $id)" );
    }
    
    # close all sockets ..
    eval {
        #delete $heap->{ $_ } for qw/ client socket /; #
        $heap->{ socket }->flush;
        #delete $heap->{ client };
    };
    $heap->{ decency }->logger->error( "Could not remove client from list after weird disconnect: $@" )
        if $@;
    
    
    return ;
}

=head1 AUTHOR

Ulrich Kautz <uk@fortrabbit.de>

=head1 COPYRIGHT

Copyright (c) 2010 the L</AUTHOR> as listed above

=head1 LICENCSE

This library is free software and may be distributed under the same terms as perl itself.

=cut



1;