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*(\[\w\d\.\-\_\=]+\@[\w\d\.\-\_]*\>?)/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;