next up previous contents
Next: ProtoWrap/POP3.pm Up: ProtoWrap program code Previous: ProtoWrap.pm   Contents


ProtoWrap/SMTP.pm

1

package ProtoWrap::SMTP;
#
# ProtoWrap::SMTP - SMTP extension to ProtoWrap
#
use ProtoWrap;
@ISA = qw(ProtoWrap);
use strict;
use vars qw($crlf);

$crlf = chr(13).chr(10);

sub new {
    my ($class,$self,@smtpSpecific,@smtpNotProtoWrap,%tmpHash);
    $class = shift;
    $self = { @_ };
    bless $self, $class;
    # Parameters accepted for SMTP that are not part of the base 
    # ProtoWrap should be stored in a temporal hash until the object is 
    # created if we do not want ProtoWrap to complain, and then added
    # to the existing object.
    # The affected attributes are:
    # maxMsgSize - the maximum message size allowed
    # blockAddrList - A reference to an array with a list of regular
    #   expressions representing blocked addresses.
    # blockBodyList - A reference to an array with a list of regular
    #   expressions representing lines to be blocked in the message body.
    # maxRcpt - The maximum number of recipients specified with 
    #   RCPT TO:
    # relayDomainList - List of domains which will be relayed - they are 
    #   required to appear either in the MAIL FROM or in the RCPT TO areas.
    #   (regex anchored to end of string)
    # relayIpList - List of IP addresses or ranges which will be accepted
    #   for relay. (regex anchored to beginning of string)
    @smtpNotProtoWrap = qw(maxMsgSize blockAddrList blockBodyList 
                           relayDomainList relayIpList maxRcpt);

    foreach (@smtpNotProtoWrap) {
        if (defined $self->{$_}) {
            $tmpHash{$_} = $self->{$_};
            delete $self->{$_};
        }
    }

    # Check for wrong parameters
    @smtpSpecific = ('testLine','testReply');


    foreach (@smtpSpecific) {
        if (defined $self->{$_}){
            $self->log(0,"Parameter $_ incompatible with SMTP");
            return undef 
        }
    }
    # Set the SMTP defined parameters needed to create a ProtoWrap
    if ($self->{destType} eq 'ip') {
        $self->{destPort} = 25 unless (defined $self->{destPort});
        $self->{destAddr} = '127.0.0.1' unless (defined $self->{destAddr});
    } elsif ($self->{destType} eq 'pipe') {
        $self->{pipeCmd} = '/usr/lib/sendmail -bs' unless (defined $self->{pipeCmd});
    }
    $self->{standalone} = 1 unless defined $self->{standalone};
    $self->{testLine} = \&defaultTestLine;
    $self->{testReply} = 1;
    $self->{logName} = 'ProtoWrap-SMTP';
    # Create the ProtoWrap. If it is undef, an error occured and we
    # should not start.
    $self = ProtoWrap->new(%$self);
    return undef if (not defined $self);
    # Add SMTP-specific attributes not allowed for base ProtoWraps, 
    # and free %tmpHash's memory
    foreach (keys(%tmpHash)) {
        $self->{$_} = $tmpHash{$_};
        delete $tmpHash{$_};
    }
    # Set the needed parameters for a SMTP ProtoWrap that could not be
    # specified before creating a ProtoWrap
    $self->{maxMsgSize} = 10000000 if (not defined $self->{maxMsgSize});
    $self->{stage}=-1;
    # Make $self a SMTP object
    bless $self, $class;
}

sub defaultTestLine {
    my ($self,$line,$socket,$who) = @_;
    if ($who == 0) {
        return &fromClient($self,$line,$socket);
    } elsif ($who == 1) {
        return &fromServer($self,$line,$socket);
    } else {

    }

}

sub fromServer {
    my($self,$line,$socket,$who,$retValue);
    ($self,$line,$socket,$who) = @_;

    $retValue = 1;

    # We strip CR and LF to do the tests, add them back at the
    # end.
    $$line =~ s/[\r\n]//g;

    if ($self->{stage} == -1) {
        # Stage -1: Waiting for the server to say hello.
        # Instead of letting the real server expose its identity,
        # we give ours. We only check if the server's response
        # starts with 2, meaning that the incoming connection 
        # should be accepted. We switch to stage 0, and signal that
        # we have still not decided to relay this message.
        $$line = '220 localhost.localdomain ESMTP ProtoWrap wrapper '.$self->version() if 
            (substr($$line,0,1) eq '2');
        $self->{relayOk} = 0;
        # If the connection comes from an authorized relaying IP,
        # allow relaying.
        
        foreach (@{$self->{relayIpList}}) {
            $self->{relayOk} = 1 if (defined $self->{srcIpAddr} && 
                                     $self->{srcIpAddr} =~ /^$_/);
        }
        $self->{stage} = 0;
    } elsif ($self->{stage} == 1) {
        # If we get a 550 <address> Access denied message from 
        # the server, and rcpts is 1, this means that the server
        # denied a sender address. We restore stage to 0.
        # If we are getting a 550 Access denied, we are almost
        # surely with 0 recipients... But double-checking does 
        # not hurt.
        if ($$line =~ /^550 .+ Access denied/) {
            $self->{stage} = 0 if ($self->{rcpts} == 0)
        }
    }

    $$line .= chr(13).chr(10);
    return $retValue;
}

sub fromClient {
    # stage indicates the current stage of the connection, thus
    # indicating what will be accepted or rejected in the ongoing
    # communication.
    #
    my($self,$line,$socket,$who,$retValue);
    ($self,$line,$socket,$who) = @_;

    $retValue = 1;
    # We strip CR and LF to do the tests, add them back at
    # the end.
    $$line =~ s/[\r\n]//g;

    #
    # Our first test should be for maxLineLength
    #
    if (length($$line) > $self->{maxLineLength} && $self->{maxLineLength} > 0) {
        $$line = substr($$line,0,$self->{maxLineLength});
        $self->log(1,"Line too long\nChopping to:\n$$line");
    }

    # Stage -1 indicates that the server has not yet talked on
    # the connection, and all traffic before this should be
    # silently dropped.
    if ($self->{stage} < 0) {
        $$line = '';
        return -1;
    }
    
    # If we are in stage 9, we should check against rejectMsg, maxMsgSize and 
    # any regexes found in blockBodyList
    if ($self->{stage} == 9) {
        if ($$line eq '.') {
            if ($self->{msgLength} > $self->{maxMsgSize} && $self->maxMsgSize != 0) {
                # Report that message is too long and end the
                # connection. Closing the socket causes the server to
                # discard the message. We then exit cleanly.
                $socket->print('550 Message length exceeded - maximum allowed: '.
                               $self->{maxMsgSize}.' bytes'.$crlf);
                $socket->close;
                $self->log(1,'Terminating connection');
                exit 0;
            } elsif ($self->{rejectMsg}) {
                $socket->print('550 Message rejected - security check failed'.$crlf);
                $socket->close;
                $self->log(1,'Terminating connection');
                exit 0;
            } else {
                # Set everything up to start processing a new message
                $self->{stage} = 0;
                $self->{relayOk} = 0;
                foreach (@{$self->{relayIpList}}) {
                    $self->{relayOk} = 1 if (defined $self->{srcIpAddr} && 
                                             $self->{srcIpAddr} =~ /^$_/);
                }
            }
        }
        if ($self->{rejectMsg}) {
            # Message has been flagged to be rejected. Pretend to continue
            # processing it. The server does not need to know about it,
            # anyway, as we are going to discard it.
            $$line = '';
            $retValue = -1;
        }

        # If we are over maxMsgSize, stop sending data to the
        # server
        if ($self->{msgLength} >= $self->{maxMsgSize}) {
            $$line = '';
            $retValue = -1;
        }
        # Check now against the blockBodyList
        foreach (@{$self->{blockBodyList}}) {
            if ($$line =~ /$_/i) {
                # If it matches, keep recieving the message (we can only
                # indicate failure after the message has been completely
                # recieved if we want to stay RFC compliant). 
                $self->{rejectMsg} = 1;
                $self->log(1,"Message rejected - Matched blockBodyList rule $_");
            }
        }

        $$line .= chr(13).chr(10);
        $self->{msgLength} += length($$line);
        return $retValue;
    }

    # At this point, we are certain that we are in a command-only
    # stage. SMTP Commands are all over four characters long.
    if (length($$line) < 4) {
        $socket->print("500 Command unrecognized: \"$$line\"$crlf");
        $$line = '';
        $retValue = 0;

        # Commands which should be allowed no matter which stage
        # are we in.
    } elsif (uc(substr($$line,0,4)) eq 'QUIT') {
        # If a QUIT is recieved, return only QUIT, chopping the
        # rest of the line.
        $$line = 'QUIT';
        $self->{stage} = 10;
    } elsif (uc(substr($$line,0,4)) eq 'RSET') {
        # If a RSET is recieved, return only RSET, chopping the
        # rest of the line.
        $$line = 'RSET';
        $self->{stage} = 0;
        $self->{relayOk} = 0;
    } elsif ($$line =~ /^HELO [\w\d\.\-\_]+/i || $$line =~ /EHLO [\w\d\.\-\_]+/i) {
        # Just allow it to reach the server. Warn if resolved name 
        # is not similar to reported name
        my $reported = $$line;
        my $srcName = $self->{srcName};
        $reported =~ s/^(HE|EH)LO\s+//i;
        $self->log(4,"Resolved ($srcName) and reported ($reported) hostnames don't match")
            if (defined $srcName && defined $reported && $srcName !~ $reported 
                && $reported !~ $srcName);

        # Convert EHLO into HELO - ProtoWrap wants the client to
        # stay in the base SMTP command set, disallowing 
        # extensions
        $$line = 'HELO '.$reported;

        # Commands which should be disallowed or handled at the
        # wrapper no matter which stage are we in (should also
        # send an error message back to the client if adequate)
    } elsif (uc(substr($$line,4)) =~ /^(VRFY|EXPN|SEND|SAML|SOML|TURN|HELP)/) {
        $socket->print("502 Sorry, we do not allow this operation$crlf");
        $$line = '';
        $retValue = 0;
    } elsif (uc(substr($$line,0,4)) eq 'NOOP') {
        $socket->print("250 OK$crlf");
        $$line = '';
        $retValue = -1;
        # Now, commands specific to a certain stage
    } elsif ((not defined $self->{stage}) || $self->{stage} == 0) {
        # First stage, 
        # Allow only MAIL FROM:<address>. While checking, delete
        # all unnecessary blank spaces.
        if ($$line =~ /^MAIL\s+FROM:/i) {
            # Is it a valid address?
	    # Note that MAIL FROM: <> must also be allowed, since it is
	    # used for administrative purposes
            if ($$line =~ 
                s/^MAIL\s+FROM:\s*(\?)/MAIL FROM:<$1>/i ||
		$$line =~ 
		s/^MAIL\s+FROM:\s*<>/MAIL FROM:<>/i ) {

                # Check against the blockAddrList and the relayDomainList
                my ($reject);
                $self->{msgFrom} = $$line;
                $reject = 0;
                $self->{msgFrom} =~ s/MAIL\s+FROM:\s*//i;
                foreach (@{$self->{blockAddrList}}) {
                    if ($self->{msgFrom} =~ /$_/i) {
                        $reject = 1;
                        $socket->print('550 '.$self->{msgFrom}."... Access denied$crlf");
                        $retValue = -1;
                        last;
                    }
                }
                foreach (@{$self->{relayDomainList}}) {
                    if ($self->{msgFrom} =~ /$_$/i) {
                        $self->{relayOk} = 1;
                    }
                }
                if ($reject == 0) {
                    $self->{stage} = 1;
                    $self->{rcpts} = 0;
                }
            } else {
                $retValue = 0;
                $$line =~ s/MAIL\s+FROM:\s*//;
                $socket->print("553 $$line... Domain name required$crlf");
            }
        } else {
            # Disallow everything else
            $retValue = 0;
            chomp $$line;
            $socket->print("500 Command unrecognized: \"$$line\"$crlf");
            $$line = '';
        }

    } elsif ($self->{stage} == 1) {
        # Second stage.
        # Allow only RCPT TO:<address> and check if we have not 
        # reached maxRcpt
        if ($$line =~ 
            s/^RCPT\s+TO:\s*(\<*[\w\d\.\-\_\=]+\@?[\w\d\.\-\_]*\>*)/RCPT TO:$1/i) {
            # Sometimes addresses are supplied surrounded by angled
            # brackets <>. We can safely strip them out.
            $$line =~ s/[\<\>]//g;
        
            # Allow relaying if no destination host is specified 
            # (local user)
            $self->{relayOk} = 1 if (index($$line,'@') == -1);
            # relayOk is true if the original sender appears in the 
            # relayDomainList
            if ($self->{relayOk} == 0) {
                # If it does not, check if the sender does
                $self->{msgTo} = $$line;
                $self->{msgTo} =~ s/RCPT\s+TO:\s*//i;
                foreach (@{$self->{relayDomainList}}) {
                    if ($self->{msgTo} =~ /$_$/i) {
                        $self->{relayOk} = 1;
                    } 
                }
            }
            # If relayOk is still 0, the recipient should not be accepted
            if ($self->{relayOk} == 0) {
                $socket->print('550 '.$self->{msgTo}.'... Relaying denied'.$crlf);
                $$line = '';
                $self->log(1,'Relaying denied from '.$self->{msgFrom}.' to '.
                           $self->{msgTo}.
                           ' from  '.$self->{srcName}.'('.$self->{srcIpAddr}.')');
                $retValue = -1;
            } else {
                if ($self->{rcpts} < $self->{maxRcpt} || $self->{maxRcpt} == 0) {
                    $self->{stage} = 2;
                    $self->{rcpts}++;
                } else {
                    $socket->print('551 Too many recipients ('.$self->{rcpts}.")$crlf");
                    $self->log(1,'Too many recipients: '.$self->{rcpts});
                    $$line = '';
                    $retValue = -1;
                }
            }
        } else {
            # Disallow everything else
            $retValue = 0;
            chomp $$line;
            $socket->print("500 Command unrecognized: \"$$line\"$crlf");
            $$line = '';
        }

    } elsif ($self->{stage} == 2) {
        # Third stage
        # Allow DATA, switching to stage 9 and resetting the 
        # message length counter, or allow for additional 
        # RCPT TO, staying in stage 2.
        if (uc(substr($$line,0,4)) eq 'DATA') {
            $self->{stage} = 9;
            $$line="DATA";
            $self->{msgLength} = 0;
            $self->{rejectMsg} = 0;
        } elsif ($$line =~ 
                 s/^RCPT\s+TO:\s*(\<*[\w\d\.\-\_\=]+\@?[\w\d\.\-\_]*\>*)/RCPT TO:$1/i) {
            # Sometimes addresses are supplied surrounded by angled
            # brackets <>. We can safely strip them out.
            $$line =~ s/[\<\>]//g;

            if ($self->{rcpts} < $self->{maxRcpt} || $self->{maxRcpt} == 0) {
                $self->{stage} = 2;
                $self->{rcpts}++;
            } else {
                $socket->print('551 Too many recipients: '.$self->{rcpts}.")$crlf");
                $self->log(1,'Too many recipients ('.$self->{rcpts});
                $$line = '';
                $retValue = -1;
            }
        } else {
            # Disallow everything else
            $retValue = 0;
            chomp $$line;
            $socket->print("500 Command unrecognized: \"$$line\"$crlf");
            $$line = '';
        }
        
    } else {
        $$line = '';
        $retValue=0;
    }
    $$line .= chr(13).chr(10);
    return $retValue;
}

1;



Gunnar Wolf
2001-03-12