#!/usr/bin/perl -w ############################################################################## ## sendEmail ## Written by: Brandon Zehm ## ## License: ## sendEmail (hereafter referred to as "program") is free software; ## you can redistribute it and/or modify it under the terms of the GNU General ## Public License as published by the Free Software Foundation; either version ## 2 of the License, or (at your option) any later version. ## Note that when redistributing modified versions of this source code, you ## must ensure that this disclaimer and the above coder's names are included ## VERBATIM in the modified code. ## ## Disclaimer: ## This program is provided with no warranty of any kind, either expressed or ## implied. It is the responsibility of the user (you) to fully research and ## comprehend the usage of this program. As with any tool, it can be misused, ## either intentionally (you're a vandal) or unintentionally (you're a moron). ## THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM ## or anything that happens because of your use (or misuse) of this program, ## including but not limited to anything you, your lawyers, or anyone else ## can dream up. And now, a relevant quote directly from the GPL: ## ## NO WARRANTY ## ## 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY ## FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN ## OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES ## PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED ## OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS ## TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE ## PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, ## REPAIR OR CORRECTION. ## ############################################################################## use strict; use IO::Socket; ######################## ## Global Variables ## ######################## my %conf = ( ## General "programName" => $0, ## The name of this program "version" => '1.52', ## The version of this program "authorName" => 'Brandon Zehm', ## Author's Name "authorEmail" => 'caspian@dotconf.net', ## Author's Email Address "timezone" => '+0000 (GMT)', ## We always use +0000 for the time zone "hostname" => 'localhost', ## Used in printmsg() for all output, and in SMTP EHLO. "debug" => 0, ## Default debug level "error" => '', ## Error messages will often be stored here ## Logging "debug" => 0, "stdout" => 1, "logging" => 0, ## If this is true the printmsg function prints to the log file "logFile" => '', ## If this is specified (form the command line via -l) this file will be used for logging. ## Network "server" => 'localhost', ## Default SMTP server "port" => 25, ## Default port "alarm" => '', ## Default timeout for connects and reads, this gets set from $opt{'timeout'} ## Email "delimiter" => "----MIME delimiter for sendEmail-" ## MIME Delimiter . rand(1000000), ## Add some randomness to the delimiter "Message-ID" => rand(1000000) . "-sendEmail", ## Message-ID for email header "authUsername" => '', ## Username used in SMTP Auth "authPassword" => '', ## Password used in SMTP Auth ); ## This hash stores the options passed on the command line via the -o option. my %opt = ( ## Addressing "reply-to" => '', ## Reply-To field ## Message "message-file" => '', ## File to read message body from "message-header" => '', ## Additional email header line "message-format" => 'normal', ## If "raw" is specified the message is sent unmodified "message-charset" => 'iso-8859-1', ## Message character-set ## Network "timeout" => 60, ## Default timeout for connects and reads, this is copied to $conf{'alarm'} later. ); ## More variables used later in the program my $CRLF = "\015\012"; my $subject = ''; my $message = ''; my $from = ''; my @to = (); my @cc = (); my @bcc = (); my @attachments = (); my @attachments_names = (); ## For printing colors to the console my ${colorRed} = "\033[31;1m"; my ${colorGreen} = "\033[32;1m"; my ${colorCyan} = "\033[36;1m"; my ${colorWhite} = "\033[37;1m"; my ${colorNormal} = "\033[m"; my ${colorBold} = "\033[1m"; my ${colorNoBold} = "\033[0m"; ## Don't use shell escape codes on Windows systems if ($^O =~ /win/i) { ${colorRed} = ""; ${colorGreen} = ""; ${colorCyan} = ""; ${colorWhite} = ""; ${colorNormal} = ""; ${colorBold} = ""; ${colorNoBold} = ""; } ############################# ## ## MAIN PROGRAM ## ############################# ## Initialize initialize(); ## Process Command Line processCommandLine(); $conf{'alarm'} = $opt{'timeout'}; ## Abort program after $conf{'alarm'} seconds to avoid infinite hangs alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 ################################################### ## Read $message from STDIN if -m was not used ## ################################################### if (!($message)) { ## Read message body from a file specified with -o message-file= if ($opt{'message-file'}) { if (! -e $opt{'message-file'}) { printmsg("ERROR => Message body file specified [$opt{'message-file'}] does not exist!", 0); printmsg("HINT => 1) check spelling of your file; 2) fully qualify the path; 3) doubble quote it", 1); quit("", 1); } if (! -r $opt{'message-file'}) { printmsg("ERROR => Message body file specified can not be read due to restricted permissions!", 0); printmsg("HINT => Check permissions on file specified to ensure it can be read", 1); quit("", 1); } if (!open(MFILE, "< " . $opt{'message-file'})) { printmsg("ERROR => Error opening message body file [$opt{'message-file'}]: $!", 0); quit("", 1); } while () { $message .= $_; } close(MFILE); } ## Read message body from STDIN else { alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 if ($conf{'stdout'}) { print "Reading message body from STDIN because the '-m' option was not used.\n"; print "If you are manually typing in a message:\n"; print " - First line must be received within $conf{'alarm'} seconds.\n" if ($^O !~ /win/i); print " - End manual input with a CTRL-D on its own line.\n\n" if ($^O !~ /win/i); print " - End manual input with a CTRL-Z on its own line.\n\n" if ($^O =~ /win/i); } while () { ## Read STDIN into $message $message .= $_; alarm(0) if ($^O !~ /win/i); ## Disable the alarm since at least one line was received } printmsg("Message input complete.", 0); } } ## Replace bare LF's with CRLF's (\012 should always have \015 with it) $message =~ s/(\015)?(\012|$)/\015\012/g; ## Replace bare CR's with CRLF's (\015 should always have \012 with it) $message =~ s/(\015)(\012|$)?/\015\012/g; ## Check message for bare periods and encode them $message =~ s/(^|$CRLF)(\.{1})($CRLF|$)/$1.$2$3/g; ## Get the current date for the email header my ($sec,$min,$hour,$mday,$mon,$year,$day) = gmtime(); $year += 1900; $mon = return_month($mon); $day = return_day($day); my $date = sprintf("%s, %s %s %d %.2d:%.2d:%.2d %s",$day, $mday, $mon, $year, $hour, $min, $sec, $conf{'timezone'}); ################################## ## Connect to the SMTP server ## ################################## printmsg("DEBUG => Connecting to $conf{'server'}:$conf{'port'}", 1); $SIG{'ALRM'} = sub { printmsg("ERROR => Timeout while connecting to $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds.", 0); printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); quit("", 1); }; alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; my $SERVER = IO::Socket::INET->new( PeerAddr => $conf{'server'}, PeerPort => $conf{'port'}, Proto => 'tcp', Autoflush => 1, timeout => $conf{'alarm'}, ); alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; ## Make sure we got connected if ( (!$SERVER) or (!$SERVER->opened()) ) { printmsg("ERROR => Connection attempt to $conf{'server'}:$conf{'port'} failed: $@", 0); printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); quit("", 1); } ######################### ## Do the SMTP Dance ## ######################### ## Read initial greeting to make sure we're talking to a live SMTP server if (SMTPchat()) { quit($conf{'error'}, 1); } ## EHLO if (SMTPchat('EHLO ' . $conf{'hostname'})) { printmsg($conf{'error'}, 0); printmsg("NOTICE => EHLO command failed, attempting HELO instead"); if (SMTPchat('HELO ' . $conf{'hostname'})) { quit($conf{'error'}, 1); } if ( $conf{'authUsername'} and $conf{'authPassword'} ) { printmsg("WARNING => The mail server does not support ESMTP or SMTP AUTH!"); } } else { ## Do SMTP Auth if required if ( $conf{'authUsername'} and $conf{'authPassword'} ) { if (SMTPchat('AUTH LOGIN')) { quit($conf{'error'}, 1); } if (SMTPchat($conf{'authUsername'})) { quit($conf{'error'}, 1); } if (SMTPchat($conf{'authPassword'})) { quit($conf{'error'}, 1); } } } ## MAIL FROM if (SMTPchat('MAIL FROM:<' .(returnAddressParts($from))[1]. '>')) { quit($conf{'error'}, 1); } ## RCPT TO my $oneRcptAccepted = 0; foreach my $rcpt (@to, @cc, @bcc) { my ($name, $address) = returnAddressParts($rcpt); if (SMTPchat('RCPT TO:<' . $address . '>')) { printmsg("WARNING => The recipient <$address> was rejected by the mail server, error follows:", 0); $conf{'error'} =~ s/^ERROR/WARNING/o; printmsg($conf{'error'}, 0); } elsif ($oneRcptAccepted == 0) { $oneRcptAccepted = 1; } } ## If no recipients were accepted we need to exit with an error. if ($oneRcptAccepted == 0) { quit("ERROR => Exiting. No recipients were accepted for delivery by the mail server.", 1); } ## DATA if (SMTPchat('DATA')) { quit($conf{'error'}, 1); } ############################### ## Build and send the body ## ############################### printmsg("INFO => Sending message body",1); ## If the message-format is raw just send the message as-is. if ($opt{'message-format'} =~ /^raw$/i) { print $SERVER $message; } ## If the message-format isn't raw, then build and send the message, else { ## Message-ID: print $SERVER 'Message-ID: <' . $conf{'Message-ID'} . '@' . $conf{'hostname'} . '>' . $CRLF; ## From: "Name" (the pointless test below is just to keep scoping correct) if ($from) { my ($name, $address) = returnAddressParts($from); print $SERVER 'From: "' . $name . '" <' . $address . '>' . $CRLF; } ## Reply-To: if ($opt{'reply-to'}) { my ($name, $address) = returnAddressParts($opt{'reply-to'}); print $SERVER 'Reply-To: "' . $name . '" <' . $address . '>' . $CRLF; } ## To: "Name" if (scalar(@to) > 0) { print $SERVER "To:"; for (my $a = 0; $a < scalar(@to); $a++) { my $msg = ""; my ($name, $address) = returnAddressParts($to[$a]); $msg = " \"$name\" <$address>"; ## If we're not on the last address add a comma to the end of the line. if (($a + 1) != scalar(@to)) { $msg .= ","; } print $SERVER $msg . $CRLF; } } ## We always want a To: line so if the only recipients were bcc'd they don't see who it was sent to else { print $SERVER "To: \"Undisclosed Recipients\" <>$CRLF"; } if (scalar(@cc) > 0) { print $SERVER "Cc:"; for (my $a = 0; $a < scalar(@cc); $a++) { my $msg = ""; my ($name, $address) = returnAddressParts($cc[$a]); $msg = " \"$name\" <$address>"; ## If we're not on the last address add a comma to the end of the line. if (($a + 1) != scalar(@cc)) { $msg .= ","; } print $SERVER $msg . $CRLF; } } print $SERVER 'Subject: ' . $subject . $CRLF; ## Subject print $SERVER 'Date: ' . $date . $CRLF; ## Date print $SERVER 'X-Mailer: sendEmail-'.$conf{'version'}.$CRLF; ## X-Mailer ## Send an additional message header line if specified if ($opt{'message-header'}) { print $SERVER $opt{'message-header'} . $CRLF; } ## Encode all messages with MIME. print $SERVER "MIME-Version: 1.0$CRLF"; print $SERVER "Content-Type: multipart/mixed; boundary=\"$conf{'delimiter'}\"$CRLF"; print $SERVER "$CRLF"; print $SERVER "This is a multi-part message in MIME format. To properly display this message you need a MIME-Version 1.0 compliant Email program.$CRLF"; print $SERVER "$CRLF"; ## Send message body print $SERVER "--$conf{'delimiter'}$CRLF"; ## If the message contains HTML change the Content-Type if ($message =~ /^\s*/i) { printmsg("Message is in HTML format", 1); print $SERVER "Content-Type: text/html;$CRLF"; } ## Otherwise it's a normal text email else { print $SERVER "Content-Type: text/plain;$CRLF"; } print $SERVER " charset=\"" . $opt{'message-charset'} . "\"$CRLF"; print $SERVER "Content-Transfer-Encoding: 7bit$CRLF"; print $SERVER $CRLF; print $SERVER $message; ## Send Attachemnts if ($attachments[0]) { ## Disable the alarm so people on modems can send big attachments alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32 ## Send the attachments foreach my $filename (@attachments) { ## This is check 2, we already checked this above, but just in case... if ( ! -f $filename ) { printmsg("ERROR => The file [$filename] doesn't exist! Email will be sent, but without that attachment.", 0); } elsif ( ! -r $filename ) { printmsg("ERROR => Couldn't open the file [$filename] for reading: $! Email will be sent, but without that attachment.", 0); } else { printmsg("DEBUG => Sending the attachment [$filename]", 1); send_attachment($filename); } } } ## End the mime encoded message print $SERVER "$CRLF--$conf{'delimiter'}--$CRLF"; } ## Tell the server we are done sending the email print $SERVER "$CRLF.$CRLF"; if (SMTPchat()) { quit($conf{'error'}, 1); } #################### # We are done!!! # #################### ## Disconnect from the server if (SMTPchat('QUIT')) { quit($conf{'error'}, 1); } close $SERVER; ####################################### ## Generate exit message/log entry ## ####################################### if ($conf{'debug'} or $conf{'logging'}) { printmsg("Generating a detailed exit message", 3); ## Put the message together my $output = "Email was sent successfully! From: <" . (returnAddressParts($from))[1] . "> "; if (scalar(@to) > 0) { $output .= "To: "; for ($a = 0; $a < scalar(@to); $a++) { $output .= "<" . (returnAddressParts($to[$a]))[1] . "> "; } } if (scalar(@cc) > 0) { $output .= "Cc: "; for ($a = 0; $a < scalar(@cc); $a++) { $output .= "<" . (returnAddressParts($cc[$a]))[1] . "> "; } } if (scalar(@bcc) > 0) { $output .= "Bcc: "; for ($a = 0; $a < scalar(@bcc); $a++) { $output .= "<" . (returnAddressParts($bcc[$a]))[1] . "> "; } } $output .= "Subject: [$subject] " if ($subject); if (scalar(@attachments_names) > 0) { $output .= "Attachment(s): "; foreach(@attachments_names) { $output .= "[$_] "; } } $output .= "Server: [$conf{'server'}:$conf{'port'}]"; ###################### # Exit the program # ###################### ## Print / Log the detailed message quit($output, 0); } else { ## Or the standard message quit("Email was sent successfully!", 0); } ############################################################################################### ## Function: initialize () ## ## Does all the script startup jibberish. ## ############################################################################################### sub initialize { ## Set STDOUT to flush immediatly after each print $| = 1; ## Intercept signals $SIG{'QUIT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; $SIG{'INT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; $SIG{'KILL'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; $SIG{'TERM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; ## ALARM and HUP signals are not supported in Win32 unless ($^O =~ /win/i) { $SIG{'HUP'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; $SIG{'ALRM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; } ## Fixup $conf{'programName'} $conf{'programName'} =~ s/(.)*[\/,\\]//; $0 = $conf{'programName'} . " " . join(" ", @ARGV); ## Fixup $conf{'hostname'} if ($conf{'hostname'} eq 'localhost') { $conf{'hostname'} = ""; if ($ENV{'HOSTNAME'}) { $conf{'hostname'} = lc($ENV{'HOSTNAME'}); } elsif ($ENV{'COMPUTERNAME'}) { $conf{'hostname'} = lc($ENV{'COMPUTERNAME'}); } else { ## Try the hostname module use Sys::Hostname; $conf{'hostname'} = lc(hostname()); } ## Assign a name of "localhost" if it can't find anything else. if (!$conf{'hostname'}) { $conf{'hostname'} = 'localhost'; } $conf{'hostname'} =~ s/\..*$//; ## Remove domain name if it's present } return(1); } ############################################################################################### ## Function: processCommandLine () ## ## Processes command line storing important data in global vars (usually %conf) ## ############################################################################################### sub processCommandLine { ############################ ## Process command line ## ############################ my @ARGS = @ARGV; ## This is so later we can re-parse the command line args later if we need to my $numargv = @ARGS; help() unless ($numargv); my $counter = 0; for ($counter = 0; $counter < $numargv; $counter++) { if ($ARGS[$counter] =~ /^-h$/i) { ## Help ## help(); } elsif ($ARGS[$counter] eq "") { ## Ignore null arguments ## Do nothing } elsif ($ARGS[$counter] =~ /^--help/) { ## Topical Help ## $counter++; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { helpTopic($ARGS[$counter]); } else { help(); } } elsif ($ARGS[$counter] =~ /^-o$/i) { ## Options specified with -o ## $counter++; ## Loop through each option passed after the -o while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { if ($ARGS[$counter] !~ /(\S+)=(\S.*)/) { printmsg("WARNING => Name/Value pair [$ARGS[$counter]] is not properly formatted", 0); printmsg("WARNING => Arguments proceeding -o should be in the form of \"name=value\"", 0); } else { if (exists($opt{$1})) { $opt{$1} = $2; printmsg("DEBUG => Assigned \$opt{} key/value: $1 => $2", 3); } else { printmsg("WARNING => Name/Value pair [$ARGS[$counter]] will be ignored: unknown key [$1]", 0); printmsg("HINT => Try the --help option to find valid command line arguments", 1); } } $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-f$/) { ## From ## $counter++; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $from = $ARGS[$counter]; } else { printmsg("WARNING => The argument after -f was not an email address!", 0); $counter--; } } elsif ($ARGS[$counter] =~ /^-t$/) { ## To ## $counter++; while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { if ($ARGS[$counter] =~ /[;,]/) { push (@to, split(/[;,]/, $ARGS[$counter])); } else { push (@to,$ARGS[$counter]); } $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-cc$/) { ## Cc ## $counter++; while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { if ($ARGS[$counter] =~ /[;,]/) { push (@cc, split(/[;,]/, $ARGS[$counter])); } else { push (@cc,$ARGS[$counter]); } $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-bcc$/) { ## Bcc ## $counter++; while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { if ($ARGS[$counter] =~ /[;,]/) { push (@bcc, split(/[;,]/, $ARGS[$counter])); } else { push (@bcc,$ARGS[$counter]); } $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-m$/) { ## Message ## $counter++; $message = ""; while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { if ($message) { $message .= " "; } $message .= $ARGS[$counter]; $counter++; } $counter--; ## Replace '\n' with $CRLF. ## This allows newlines with messages sent on the command line $message =~ s/\\n/$CRLF/g; } elsif ($ARGS[$counter] =~ /^-u$/) { ## Subject ## $counter++; $subject = ""; while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { if ($subject) { $subject .= " "; } $subject .= $ARGS[$counter]; $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-s$/) { ## Server ## $counter++; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'server'} = $ARGS[$counter]; if ($conf{'server'} =~ /:/) { ## Port ## ($conf{'server'},$conf{'port'}) = split(":",$conf{'server'}); } } else { printmsg("WARNING - The argument after -s was not the server!", 0); $counter--; } } elsif ($ARGS[$counter] =~ /^-a$/) { ## Attachments ## $counter++; while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { push (@attachments,$ARGS[$counter]); $counter++; } $counter--; } elsif ($ARGS[$counter] =~ /^-xu$/) { ## AuthSMTP Username ## $counter++; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'authUsername'} = $ARGS[$counter]; my $tmp = substr(pack('u', $conf{'authUsername'}), 1); ## Convert the string to uuencoded text chop($tmp); $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 $conf{'authUsername'} = $tmp; } else { printmsg("WARNING => The argument after -xu was not valid username!", 0); $counter--; } } elsif ($ARGS[$counter] =~ /^-xp$/) { ## AuthSMTP Password ## $counter++; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'authPassword'} = $ARGS[$counter]; my $tmp = substr(pack('u', $conf{'authPassword'}), 1); ## Convert the binary to uuencoded text chop($tmp); $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 $conf{'authPassword'} = $tmp; } else { printmsg("WARNING => The argument after -xp was not valid password!", 0); $counter--; } } elsif ($ARGS[$counter] =~ /^-l$/) { ## Logging ## $counter++; $conf{'logging'} = 1; if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'logFile'} = $ARGS[$counter]; } else { printmsg("WARNING - The argument after -l was not the log file!", 0); $counter--; } } elsif ($ARGS[$counter] =~ s/^-v+//i) { ## Verbosity ## my $tmp = (length($&) - 1); $conf{'debug'} += $tmp; } elsif ($ARGS[$counter] =~ /^-q$/) { ## Quiet ## $conf{'stdout'} = 0; } else { printmsg("Error: \"$ARGS[$counter]\" is not a recognized option!", 0); help(); } } ################################################### ## Verify required variables are set correctly ## ################################################### if (!$conf{'server'}) { $conf{'server'} = 'localhost'; } if (!$conf{'port'}) { $conf{'port'} = 25; } if (!$from) { quit("ERROR => You must specify a 'from' field! Try --help.", 1); } if ( ((scalar(@to)) + (scalar(@cc)) + (scalar(@bcc))) <= 0) { quit("ERROR => You must specify at least one recipient via -t, -cc, or -bcc", 1); } ## Make sure email addresses look OK. foreach my $addr (@to, @cc, @bcc, $from, $opt{'reply-to'}) { if ($addr) { if (!returnAddressParts($addr)) { printmsg("ERROR => Can't use improperly formatted email address: $addr", 0); printmsg("HINT => Try viewing the extended help on addressing with \"--help addressing\"", 1); quit("", 1); } } } ## Make sure all attachments exist. foreach my $file (@attachments) { if ( (! -f $file) or (! -r $file) ) { printmsg("ERROR => The attachment [$file] doesn't exist!", 0); printmsg("HINT => Try specifying the full path to the file or reading extended help with \"--help message\"", 1); quit("", 1); } } if ($conf{'logging'} and (!$conf{'logFile'})) { quit("ERROR => You used -l to enable logging but didn't specify a log file!", 1); } if ( $conf{'authUsername'} ) { if (!$conf{'authPassword'}) { quit ("ERROR => You must supply both a username and a password to use SMTP auth.",1); } } ## Return 0 errors return(0); } ## getline($socketRef) sub getline { my ($socketRef) = @_; local ($/) = "\r\n"; return $$socketRef->getline; } ## Receive a (multiline?) SMTP response from ($socketRef) sub getResponse { my ($socketRef) = @_; my ($tmp, $reply); local ($/) = "\r\n"; return undef unless defined($tmp = getline($socketRef)); return("getResponse() socket is not open") unless ($$socketRef->opened); ## Keep reading lines if it's a multi-line response while ($tmp =~ /^\d{3}-/o) { $reply .= $tmp; return undef unless defined($tmp = getline($socketRef)); } $reply .= $tmp; $reply =~ s/\r?\n$//o; return $reply; } ############################################################################################### ## Function: SMTPchat ( [string $command] ) ## ## Description: Sends $command to the SMTP server (on SERVER) and awaits a successfull ## reply form the server. If the server returns an error, or does not reply ## within $conf{'alarm'} seconds an error is generated. ## NOTE: $command is optional, if no command is specified then nothing will ## be sent to the server, but a valid response is still required from the server. ## ## Input: [$command] A (optional) valid SMTP command (ex. "HELO") ## ## ## Output: Returns zero on success, or non-zero on error. ## Error messages will be stored in $conf{'error'} ## ## ## Example: SMTPchat ("HELO mail.isp.net"); ############################################################################################### sub SMTPchat { my ($command) = @_; printmsg("INFO => Sending: \t$command", 1) if ($command); ## Send our command print $SERVER "$command$CRLF" if ($command); ## Read a response from the server $SIG{'ALRM'} = sub { $conf{'error'} = "alarm"; $SERVER->close(); }; alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; my $result = getResponse(\$SERVER); alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; ## Generate an alert if we timed out if ($conf{'error'} eq "alarm") { $conf{'error'} = "ERROR => Timeout while reading from $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds."; return(1); } ## Make sure the server actually responded if (!$result) { $conf{'error'} = "ERROR => $conf{'server'}:$conf{'port'} returned a zero byte response to our query."; return(2); } ## Validate the response if (evalSMTPresponse($result)) { ## conf{'error'} will already be set here return(2); } ## Print the success messsage printmsg($conf{'error'}, 1); ## Return Success return(0); } ############################################################################################### ## Function: evalSMTPresponse (string $message ) ## ## Description: Searches $message for either an SMTP success or error code, and returns ## 0 on success, and the actual error code on error. ## ## ## Input: $message Data received from a SMTP server (ex. "220 ## ## ## Output: Returns zero on success, or non-zero on error. ## Error messages will be stored in $conf{'error'} ## ## ## Example: SMTPchat ("HELO mail.isp.net"); ############################################################################################### sub evalSMTPresponse { my ($message) = @_; ## Validate input if (!$message) { $conf{'error'} = "ERROR => No message was passed to evalSMTPresponse(). What happened?"; return(1) } printmsg("DEBUG => evalSMTPresponse() - Checking for SMTP success or error status in the message: $message ", 3); ## Look for a SMTP success code if ($message =~ /^([23]\d\d)/) { printmsg("DEBUG => evalSMTPresponse() - Found SMTP success code: $1", 2); $conf{'error'} = "SUCCESS => Received: \t$message"; return(0); } ## Look for a SMTP error code if ($message =~ /^([45]\d\d)/) { printmsg("DEBUG => evalSMTPresponse() - Found SMTP error code: $1", 2); $conf{'error'} = "ERROR => Received: \t$message"; return($1); } ## If no SMTP codes were found return an error of 1 $conf{'error'} = "ERROR => Received a message with no success or error code. The message received was: $message"; return(2); } ######################################################### # SUB: &return_month(0,1,etc) # returns the name of the month that corrosponds # with the number. returns 0 on error. ######################################################### sub return_month { my $x = $_[0]; if ($x == 0) { return 'Jan'; } if ($x == 1) { return 'Feb'; } if ($x == 2) { return 'Mar'; } if ($x == 3) { return 'Apr'; } if ($x == 4) { return 'May'; } if ($x == 5) { return 'Jun'; } if ($x == 6) { return 'Jul'; } if ($x == 7) { return 'Aug'; } if ($x == 8) { return 'Sep'; } if ($x == 9) { return 'Oct'; } if ($x == 10) { return 'Nov'; } if ($x == 11) { return 'Dec'; } return (0); } ######################################################### # SUB: &return_day(0,1,etc) # returns the name of the day that corrosponds # with the number. returns 0 on error. ######################################################### sub return_day { my $x = $_[0]; if ($x == 0) { return 'Sun'; } if ($x == 1) { return 'Mon'; } if ($x == 2) { return 'Tue'; } if ($x == 3) { return 'Wed'; } if ($x == 4) { return 'Thu'; } if ($x == 5) { return 'Fri'; } if ($x == 6) { return 'Sat'; } return (0); } ############################################################################################### ## Function: returnAddressParts(string $address) ## ## Description: Returns a two element array containing the "Name" and "Address" parts of ## an email address. ## ## Example: "Brandon Zehm " ## would return: ("Brandon Zehm", "caspian@dotconf.net"); ## ## "caspian@dotconf.net" ## would return: ("caspian@dotconf.net", "caspian@dotconf.net") ############################################################################################### sub returnAddressParts { my $input = $_[0]; my $name = ""; my $address = ""; ## Make sure to fail if it looks totally invalid if ($input !~ /(\S+\@\S+)/) { $conf{'error'} = "ERROR => The address [$input] doesn't look like a valid email address, ignoring it"; return(undef()); } ## Check 1, should find addresses like: "Brandon Zehm " elsif ($input =~ /^\s*(\S(.*\S)?)\s*<(\S+\@\S+)>/o) { ($name, $address) = ($1, $3); } ## Otherwise if that failed, just get the address: elsif ($input =~ /<(\S+\@\S+)>/o) { $name = $address = $1; } ## Or maybe it was formatted this way: caspian@dotconf.net elsif ($input =~ /(\S+\@\S+)/o) { $name = $address = $1; } ## Something stupid happened, just return an error. unless ($name and $address) { printmsg("ERROR => Couldn't parse the address: $input", 0); printmsg("HINT => If you think this should work, consider reporting this as a bug to $conf{'authorEmail'}", 1); return(undef()); } ## Make sure there aren't invalid characters in the address, and return it. my $ctrl = '\000-\037'; my $nonASCII = '\x80-\xff'; if ($address =~ /[<> ,;:"'\[\]\\$ctrl$nonASCII]/) { printmsg("WARNING => The address [$address] seems to contain invalid characters: continuing anyway", 0); } return($name, $address); } ######################################################### # SUB: send_attachment("/path/filename") # Sends the mime headers and base64 encoded file # to the email server. ######################################################### sub send_attachment { my ($filename) = @_; ## Get filename passed my (@fields, $y, $filename_name, $encoding, ## Local variables @attachlines, $content_type); my $bin = 1; @fields = split(/\/|\\/, $filename); ## Get the actual filename without the path $filename_name = pop(@fields); push @attachments_names, $filename_name; ## FIXME: This is only used later for putting in the log file ########################## ## Autodetect Mime Type ## ########################## @fields = split(/\./, $filename_name); $encoding = $fields[$#fields]; if ($encoding =~ /txt|text|log|conf|^c$|cpp|^h$|inc|m3u/i) { $content_type = 'text/plain'; } elsif ($encoding =~ /html|htm|shtml|shtm|asp|php|cfm/i) { $content_type = 'text/html'; } elsif ($encoding =~ /sh$/i) { $content_type = 'application/x-sh'; } elsif ($encoding =~ /tcl/i) { $content_type = 'application/x-tcl'; } elsif ($encoding =~ /pl$/i) { $content_type = 'application/x-perl'; } elsif ($encoding =~ /js$/i) { $content_type = 'application/x-javascript'; } elsif ($encoding =~ /man/i) { $content_type = 'application/x-troff-man'; } elsif ($encoding =~ /gif/i) { $content_type = 'image/gif'; } elsif ($encoding =~ /jpg|jpeg|jpe|jfif|pjpeg|pjp/i) { $content_type = 'image/jpeg'; } elsif ($encoding =~ /tif|tiff/i) { $content_type = 'image/tiff'; } elsif ($encoding =~ /xpm/i) { $content_type = 'image/x-xpixmap'; } elsif ($encoding =~ /bmp/i) { $content_type = 'image/x-MS-bmp'; } elsif ($encoding =~ /pcd/i) { $content_type = 'image/x-photo-cd'; } elsif ($encoding =~ /png/i) { $content_type = 'image/png'; } elsif ($encoding =~ /aif|aiff/i) { $content_type = 'audio/x-aiff'; } elsif ($encoding =~ /wav/i) { $content_type = 'audio/x-wav'; } elsif ($encoding =~ /mp2|mp3|mpa/i) { $content_type = 'audio/x-mpeg'; } elsif ($encoding =~ /ra$|ram/i) { $content_type = 'audio/x-pn-realaudio'; } elsif ($encoding =~ /mpeg|mpg/i) { $content_type = 'video/mpeg'; } elsif ($encoding =~ /mov|qt$/i) { $content_type = 'video/quicktime'; } elsif ($encoding =~ /avi/i) { $content_type = 'video/x-msvideo'; } elsif ($encoding =~ /zip/i) { $content_type = 'application/x-zip-compressed'; } elsif ($encoding =~ /tar/i) { $content_type = 'application/x-tar'; } elsif ($encoding =~ /jar/i) { $content_type = 'application/java-archive'; } elsif ($encoding =~ /exe|bin/i) { $content_type = 'application/octet-stream'; } elsif ($encoding =~ /ppt|pot|ppa|pps|pwz/i) { $content_type = 'application/vnd.ms-powerpoint'; } elsif ($encoding =~ /mdb|mda|mde/i) { $content_type = 'application/vnd.ms-access'; } elsif ($encoding =~ /xls|xlt|xlm|xld|xla|xlc|xlw|xll/i) { $content_type = 'application/vnd.ms-excel'; } elsif ($encoding =~ /doc|dot/i) { $content_type = 'application/msword'; } elsif ($encoding =~ /rtf/i) { $content_type = 'application/rtf'; } elsif ($encoding =~ /pdf/i) { $content_type = 'application/pdf'; } elsif ($encoding =~ /tex/i) { $content_type = 'application/x-tex'; } elsif ($encoding =~ /latex/i) { $content_type = 'application/x-latex'; } elsif ($encoding =~ /vcf/i) { $content_type = 'application/x-vcard'; } else { $content_type = 'application/octet-stream'; } ############################ ## Process the attachment ## ############################ ##################################### ## Generate and print MIME headers ## ##################################### $y = "$CRLF--$conf{'delimiter'}$CRLF"; $y .= "Content-Type: $content_type;$CRLF"; $y .= " name=\"$filename_name\"$CRLF"; $y .= "Content-Transfer-Encoding: base64$CRLF"; $y .= "Content-Disposition: attachment; filename=\"$filename_name\"$CRLF"; $y .= "$CRLF"; print $SERVER $y; ########################################################### ## Convert the file to base64 and print it to the server ## ########################################################### open (FILETOATTACH, $filename) || do { printmsg("ERROR => Opening the file [$filename] for attachment failed with the error: $!", 0); return(1); }; binmode(FILETOATTACH); ## Hack to make Win32 work my $res = ""; my $tmp = ""; my $base64 = ""; while () { ## Read a line from the (binary) file $res .= $_; ################################### ## Convert binary data to base64 ## ################################### while ($res =~ s/(.{45})//s) { ## Get 45 bytes from the binary string $tmp = substr(pack('u', $&), 1); ## Convert the binary to uuencoded text chop($tmp); $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 $base64 .= $tmp; } ################################ ## Print chunks to the server ## ################################ while ($base64 =~ s/(.{76})//s) { print $SERVER "$1$CRLF"; } } ################################### ## Encode and send the leftovers ## ################################### my $padding = ""; if ( ($res) and (length($res) >= 1) ) { $padding = (3 - length($res) % 3) % 3; ## Set flag if binary data isn't divisible by 3 $res = substr(pack('u', $res), 1); ## Convert the binary to uuencoded text chop($res); $res =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 } ############################ ## Fix padding at the end ## ############################ $res = $base64 . $res; ## Get left overs from above $res =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set if ($res) { while ($res =~ s/(.{1,76})//s) { ## Send it to the email server. print $SERVER "$1$CRLF"; } } close (FILETOATTACH) || do { printmsg("ERROR - Closing the filehandle for file [$filename] failed with the error: $!", 0); return(2); }; ## Return 0 errors return(0); } ############################################################################################### ## Function: printmsg (string $message, int $level) ## ## Description: Handles all messages - printing them to the screen only if the messages ## $level is >= the global debug level. If $conf{'logFile'} is defined it ## will also log the message to that file. ## ## Input: $message A message to be printed, logged, etc. ## $level The debug level of the message. If ## not defined 0 will be assumed. 0 is ## considered a normal message, 1 and ## higher is considered a debug message. ## ## Output: Prints to STDOUT ## ## Assumptions: $conf{'hostname'} should be the name of the computer we're running on. ## $conf{'stdout'} should be set to 1 if you want to print to stdout ## $conf{'logFile'} should be a full path to a log file if you want that ## $conf{'syslog'} should be 1 if you want to syslog, the syslog() function ## written by Brandon Zehm should be present. ## $conf{'debug'} should be an integer between 0 and 10. ## ## Example: printmsg("WARNING: We believe in generic error messages... NOT!", 0); ############################################################################################### sub printmsg { ## Assign incoming parameters to variables my ( $message, $level ) = @_; ## Make sure input is sane $level = 0 if (!defined($level)); $message =~ s/\s+$//sgo; $message =~ s/\r?\n/, /sgo; ## Continue only if the debug level of the program is >= message debug level. if ($conf{'debug'} >= $level) { ## Get the date in the format: Dec 3 11:14:04 my ($sec, $min, $hour, $mday, $mon) = localtime(); $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon]; my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec); ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true. if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) { print "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; } ## Print to the log file if $conf{'logging'} is true if ($conf{'logFile'}) { if (openLogFile($conf{'logFile'})) { $conf{'logFile'} = ""; printmsg("ERROR => Opening the file [$conf{'logFile'}] for appending returned the error: $!", 1); } print LOGFILE "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; } } ## Return 0 errors return(0); } ############################################################################################### ## FUNCTION: ## openLogFile ( $filename ) ## ## ## DESCRIPTION: ## Opens the file $filename and attaches it to the filehandle "LOGFILE". Returns 0 on success ## and non-zero on failure. Error codes are listed below, and the error message gets set in ## global variable $!. ## ## ## Example: ## openFile ("/var/log/sendEmail.log"); ## ############################################################################################### sub openLogFile { ## Get the incoming filename my $filename = $_[0]; ## Make sure our file exists, and if the file doesn't exist then create it if ( ! -f $filename ) { print STDERR "NOTICE: The log file [$filename] does not exist. Creating it now with mode [0600].\n" if ($conf{'stdout'}); open (LOGFILE, ">>$filename"); close LOGFILE; chmod (0600, $filename); } ## Now open the file and attach it to a filehandle open (LOGFILE,">>$filename") or return (1); ## Put the file into non-buffering mode select LOGFILE; $| = 1; select STDOUT; ## Return success return(0); } ############################################################################################### ## Function: quit (string $message, int $errorLevel) ## ## Description: Exits the program, optionally printing $message. It ## returns an exit error level of $errorLevel to the ## system (0 means no errors, and is assumed if empty.) ## ## Example: quit("Exiting program normally", 0); ############################################################################################### sub quit { my %incoming = (); ( $incoming{'message'}, $incoming{'errorLevel'} ) = @_; $incoming{'errorLevel'} = 0 if (!defined($incoming{'errorLevel'})); ## Print exit message if ($incoming{'message'}) { printmsg($incoming{'message'}, 0); } ## Exit exit($incoming{'errorLevel'}); } ############################################################################################### ## Function: help () ## ## Description: For all those newbies ;) ## Prints a help message and exits the program. ## ############################################################################################### sub help { exit(1) if (!$conf{'stdout'}); print <${colorNoBold} Synopsis: $conf{'programName'} -f ADDRESS [options] ${colorRed}Required:${colorNormal} -f ADDRESS from (sender) email address * At least one recipient required via -t, -cc, or -bcc * Message body required via -m, STDIN, or -o message-file=FILE ${colorGreen}Common:${colorNormal} -t ADDRESS [ADDR ...] to email address(es) -u SUBJECT message subject -m MESSAGE message body -s SERVER[:PORT] smtp mail relay, default is $conf{'server'}:$conf{'port'} ${colorGreen}Optional:${colorNormal} -a FILE [FILE ...] file attachment(s) -cc ADDRESS [ADDR ...] cc email address(es) -bcc ADDRESS [ADDR ...] bcc email address(es) ${colorGreen}Paranormal:${colorNormal} -xu USERNAME authentication user (for SMTP authentication) -xp PASSWORD authentication password (for SMTP authentication) -l LOGFILE log to the specified file -v verbosity, use multiple times for greater effect -q be quiet (no stdout output) -o NAME=VALUE see extended help topic "misc" for details ${colorGreen}Help:${colorNormal} --help TOPIC The following extended help topics are available: addressing explain addressing and related options message explain message body input and related options misc explain -xu, -xp, and others networking explain -s, etc output explain logging and other output options EOM exit(1); } ############################################################################################### ## Function: helpTopic ($topic) ## ## Description: For all those newbies ;) ## Prints a help message and exits the program. ## ############################################################################################### sub helpTopic { exit(1) if (!$conf{'stdout'}); my ($topic) = @_; CASE: { ## ADDRESSING ($topic eq 'addressing') && do { print <" Just Address: "john.doe\@gmail.com" The "Full Name" method is useful if you want a name, rather than a plain email address, to be displayed in the recipient's From, To, or Cc fields when they view the message. ${colorGreen}Multiple Recipients${colorNormal} The -t, -cc, and -bcc options each accept multiple addresses. They may be specified by separating them by either a white space, comma, or semi-colon separated list. You may also specify the -t, -cc, and -bcc options multiple times, each occurance will append the new recipients to the respective list. Examples: (I used "-t" in these examples, but it can be "-cc" or "-bcc" as well) * Space separated list: -t jane.doe\@yahoo.com "John Doe " * Semi-colon separated list: -t "jane.doe\@yahoo.com; John Doe " * Comma separated list: -t "jane.doe\@yahoo.com, John Doe " * Multiple -t, -cc, or -bcc options: -t "jane.doe\@yahoo.com" -t "John Doe " EOM last CASE; }; ## MESSAGE ($topic eq 'message') && do { print <" and sendEmail will properly label the mime header so MUAs properly render the message. -o message-file=FILE This option is one of three methods that allow you to specify the message body for your email. To use this option simply specify a text file containing the body of your email message. Examples: -o message-file=/root/message.txt -o message-file="C:\\Program Files\\output.txt" -o message-header=EMAIL HEADER This option allows you to specify an additional single line to insert into the email headers. Do not use this unless you know what you are doing! Example: To scare a Microsoft Outlook user you may want to try this: -o message-header="X-Message-Flag: This message contains illegal content" -o message-format=raw This option instructs sendEmail to assume the message is already a complete email message. SendEmail will not generate any headers and will transmit the message as-is to the remote SMTP server. Due to the nature of this option the following command line options will be ignored when this one is used: -u SUBJECT -o message-header=EMAIL HEADER -o message-charset=CHARSET -a ATTACHMENT -o message-charset=CHARSET This option allows you to specify the character-set for the message body. The default is iso-8859-1. ${colorGreen}The Message Body${colorNormal} The message body may be specified in one of three ways: 1) Via the -m MESSAGE command line option. Example: -m "This is the message body" 2) By putting the message body in a file and using the -o message-file=FILE command line option. Example: -o message-file=/root/message.txt 3) By piping the message body to sendEmail when nither of the above command line options were specified. Example: grep "ERROR" /var/log/messages | sendEmail -t you\@domain.com ... If the message body begins with "" then the message will be treated as an HTML message and the MIME headers will be written so that a HTML capable email client will display the message in it's HTML form. Any of the above methods may be used with the -o message-format=raw option to deliver an already complete email message. EOM last CASE; }; ## MISC ($topic eq 'misc') && do { print < The help topic specified is not valid!", 1); }; exit(1); }