Subversion Repositories DevTools

Rev

Rev 4994 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
3410 dpurdie 1
#!/usr/bin/perl -w
2
##############################################################################
3
## sendEmail
4
## Written by: Brandon Zehm <caspian@dotconf.net>
5
##
6
## License:
7
##  sendEmail (hereafter referred to as "program") is free software;
8
##  you can redistribute it and/or modify it under the terms of the GNU General
9
##  Public License as published by the Free Software Foundation; either version
10
##  2 of the License, or (at your option) any later version.
11
##  When redistributing modified versions of this source code it is recommended
12
##  that that this disclaimer and the above coder's names are included in the
13
##  modified code.
14
##
15
## Disclaimer:
16
##  This program is provided with no warranty of any kind, either expressed or
17
##  implied.  It is the responsibility of the user (you) to fully research and
18
##  comprehend the usage of this program.  As with any tool, it can be misused,
19
##  either intentionally (you're a vandal) or unintentionally (you're a moron).
20
##  THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM
21
##  or anything that happens because of your use (or misuse) of this program,
22
##  including but not limited to anything you, your lawyers, or anyone else
23
##  can dream up.  And now, a relevant quote directly from the GPL:
24
##
25
## NO WARRANTY
26
##
27
##  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
28
##  FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
29
##  OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
30
##  PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
31
##  OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
32
##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
33
##  TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
34
##  PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
35
##  REPAIR OR CORRECTION.
36
##
37
##############################################################################
38
use strict;
39
use IO::Socket;
40
 
41
 
42
########################
43
##  Global Variables  ##
44
########################
45
 
46
my %conf = (
47
    ## General
48
    "programName"          => $0,                                  ## The name of this program
49
    "version"              => '1.56',                              ## The version of this program
50
    "authorName"           => 'Brandon Zehm',                      ## Author's Name
51
    "authorEmail"          => 'caspian@dotconf.net',               ## Author's Email Address
52
    "timezone"             => '+0000',                             ## We always use +0000 for the time zone
53
    "hostname"             => 'changeme',                          ## Used in printmsg() for all output (is updated later in the script).
54
    "debug"                => 0,                                   ## Default debug level
55
    "error"                => '',                                  ## Error messages will often be stored here
56
 
57
    ## Logging
58
    "stdout"               => 1,
59
    "logging"              => 0,                                   ## If this is true the printmsg function prints to the log file
60
    "logFile"              => '',                                  ## If this is specified (form the command line via -l) this file will be used for logging.
61
 
62
    ## Network
4994 dpurdie 63
#   "server"               => 'smtp-au.vix.local',                 ## Default SMTP server
3413 dpurdie 64
    "server"               => 'localhost',                         ## Default SMTP server
3410 dpurdie 65
    "port"                 => 25,                                  ## Default port
66
    "bindaddr"             => '',                                  ## Default local bind address
67
    "alarm"                => '',                                  ## Default timeout for connects and reads, this gets set from $opt{'timeout'}
68
    "tls_client"           => 0,                                   ## If TLS is supported by the client (us)
69
    "tls_server"           => 0,                                   ## If TLS is supported by the remote SMTP server
70
 
71
    ## Email
72
    "delimiter"            => "----MIME delimiter for sendEmail-"  ## MIME Delimiter
73
                              . rand(1000000),                     ## Add some randomness to the delimiter
74
    "Message-ID"           => rand(1000000) . "-sendEmail",        ## Message-ID for email header
75
 
76
);
77
 
78
 
79
## This hash stores the options passed on the command line via the -o option.
80
my %opt = (
81
    ## Addressing
82
    "reply-to"             => '',                                  ## Reply-To field
83
 
84
    ## Message
85
    "message-file"         => '',                                  ## File to read message body from
86
    "message-header"       => '',                                  ## Additional email header line(s)
87
    "message-format"       => 'normal',                            ## If "raw" is specified the message is sent unmodified
88
    "message-charset"      => 'iso-8859-1',                        ## Message character-set
89
    "message-content-type" => 'auto',                              ## auto, text, html or an actual string to put into the content-type header.
90
 
91
    ## Network
92
    "timeout"              => 60,                                  ## Default timeout for connects and reads, this is copied to $conf{'alarm'} later.
93
    "fqdn"                 => 'changeme',                          ## FQDN of this machine, used during SMTP communication (is updated later in the script).
94
 
95
    ## eSMTP
96
    "username"             => '',                                  ## Username used in SMTP Auth
97
    "password"             => '',                                  ## Password used in SMTP Auth
5253 dpurdie 98
     "tls"                 => 'no',                                ## Enable or disable TLS support.  Options: auto, yes, no. VIX: Was auto
3410 dpurdie 99
 
100
);
101
 
102
## More variables used later in the program
103
my $SERVER;
104
my $CRLF        = "\015\012";
105
my $subject     = '';
106
my $header      = '';
107
my $message     = '';
108
my $from        = '';
109
my @to          = ();
110
my @cc          = ();
111
my @bcc         = ();
112
my @attachments = ();
113
my @attachments_names = ();
114
 
115
## For printing colors to the console
116
my ${colorRed}    = "\033[31;1m";
117
my ${colorGreen}  = "\033[32;1m";
118
my ${colorCyan}   = "\033[36;1m";
119
my ${colorWhite}  = "\033[37;1m";
120
my ${colorNormal} = "\033[m";
121
my ${colorBold}   = "\033[1m";
122
my ${colorNoBold} = "\033[0m";
123
 
124
## Don't use shell escape codes on Windows systems
125
if ($^O =~ /win/i) {
126
    ${colorRed} = ${colorGreen} = ${colorCyan} = ${colorWhite} = ${colorNormal} = ${colorBold} = ${colorNoBold} = "";
127
}
128
 
129
## Load IO::Socket::SSL if it's available
130
eval    { require IO::Socket::SSL; };
131
if ($@) { $conf{'tls_client'} = 0; }
132
else    { $conf{'tls_client'} = 1; }
133
 
134
 
135
 
136
 
137
 
138
 
139
#############################
140
##                          ##
141
##      FUNCTIONS            ##
142
##                          ##
143
#############################
144
 
145
 
146
 
147
 
148
 
149
###############################################################################################
150
##  Function: initialize ()
151
##  
152
##  Does all the script startup jibberish.
153
##  
154
###############################################################################################
155
sub initialize {
156
 
157
    ## Set STDOUT to flush immediatly after each print  
158
    $| = 1;
159
 
160
    ## Intercept signals
161
    $SIG{'QUIT'}  = sub { quit("EXITING: Received SIG$_[0]", 1); };
162
    $SIG{'INT'}   = sub { quit("EXITING: Received SIG$_[0]", 1); };
163
    $SIG{'KILL'}  = sub { quit("EXITING: Received SIG$_[0]", 1); };
164
    $SIG{'TERM'}  = sub { quit("EXITING: Received SIG$_[0]", 1); };
165
 
166
    ## ALARM and HUP signals are not supported in Win32
167
    unless ($^O =~ /win/i) {
168
        $SIG{'HUP'}   = sub { quit("EXITING: Received SIG$_[0]", 1); };
169
        $SIG{'ALRM'}  = sub { quit("EXITING: Received SIG$_[0]", 1); };
170
    }
171
 
172
    ## Fixup $conf{'programName'}
173
    $conf{'programName'} =~ s/(.)*[\/,\\]//;
174
    $0 = $conf{'programName'} . " " . join(" ", @ARGV);
175
 
176
    ## Fixup $conf{'hostname'} and $opt{'fqdn'}
177
    if ($opt{'fqdn'} eq 'changeme') { $opt{'fqdn'} = get_hostname(1); }
178
    if ($conf{'hostname'} eq 'changeme') { $conf{'hostname'} = $opt{'fqdn'}; $conf{'hostname'} =~ s/\..*//; }
179
 
180
    return(1);
181
}
182
 
183
 
184
 
185
 
186
 
187
 
188
 
189
 
190
 
191
 
192
 
193
 
194
 
195
 
196
 
197
###############################################################################################
198
##  Function: processCommandLine ()
199
##  
200
##  Processes command line storing important data in global vars (usually %conf)
201
##  
202
###############################################################################################
203
sub processCommandLine {
204
 
205
 
206
    ############################
207
    ##  Process command line  ##
208
    ############################
209
 
210
    my @ARGS = @ARGV;  ## This is so later we can re-parse the command line args later if we need to
211
    my $numargv = @ARGS;
212
    help() unless ($numargv);
213
    my $counter = 0;
214
 
215
    for ($counter = 0; $counter < $numargv; $counter++) {
216
 
217
        if ($ARGS[$counter] =~ /^-h$/i) {                    ## Help ##
218
            help();
219
        }
220
 
221
        elsif ($ARGS[$counter] eq "") {                      ## Ignore null arguments
222
            ## Do nothing
223
        }
224
 
225
        elsif ($ARGS[$counter] =~ /^--help/) {               ## Topical Help ##
226
            $counter++;
227
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
228
                helpTopic($ARGS[$counter]);
229
            }
230
            else {
231
                help();
232
            }
233
        }
234
 
235
        elsif ($ARGS[$counter] =~ /^-o$/i) {                 ## Options specified with -o ##
236
            $counter++;
237
            ## Loop through each option passed after the -o
238
            while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
239
 
240
                if ($ARGS[$counter] !~ /(\S+)=(\S.*)/) {
241
                    printmsg("WARNING => Name/Value pair [$ARGS[$counter]] is not properly formatted", 0);
242
                    printmsg("WARNING => Arguments proceeding -o should be in the form of \"name=value\"", 0);
243
                }
244
                else {
245
                    if (exists($opt{$1})) {
246
                        if ($1 eq 'message-header') {
247
                            $opt{$1} .= $2 . $CRLF;
248
                        }
249
                        else {
250
                            $opt{$1} = $2;
251
                        }
252
                        printmsg("DEBUG => Assigned \$opt{} key/value: $1 => $2", 3);
253
                    }
254
                    else {
255
                        printmsg("WARNING => Name/Value pair [$ARGS[$counter]] will be ignored: unknown key [$1]", 0);
256
                        printmsg("HINT => Try the --help option to find valid command line arguments", 1);
257
                    }
258
                }
259
                $counter++;
260
            }   $counter--;
261
        }
262
 
263
        elsif ($ARGS[$counter] =~ /^-f$/) {                  ## From ##
264
            $counter++;
265
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $from = $ARGS[$counter]; }
266
            else { printmsg("WARNING => The argument after -f was not an email address!", 0); $counter--; }
267
        }
268
 
269
        elsif ($ARGS[$counter] =~ /^-t$/) {                  ## To ##
270
            $counter++;
271
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
272
                if ($ARGS[$counter] =~ /[;,]/) {
273
                    push (@to, split(/[;,]/, $ARGS[$counter]));
274
                }
275
                else {
276
                    push (@to,$ARGS[$counter]);
277
                }
278
                $counter++;
279
            }   $counter--;
280
        }
281
 
282
        elsif ($ARGS[$counter] =~ /^-cc$/) {                 ## Cc ##
283
            $counter++;
284
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
285
                if ($ARGS[$counter] =~ /[;,]/) {
286
                    push (@cc, split(/[;,]/, $ARGS[$counter]));
287
                }
288
                else {
289
                    push (@cc,$ARGS[$counter]);
290
                }
291
                $counter++;
292
            }   $counter--;
293
        }
294
 
295
        elsif ($ARGS[$counter] =~ /^-bcc$/) {                ## Bcc ##
296
            $counter++;
297
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
298
                if ($ARGS[$counter] =~ /[;,]/) {
299
                    push (@bcc, split(/[;,]/, $ARGS[$counter]));
300
                }
301
                else {
302
                    push (@bcc,$ARGS[$counter]);
303
                }
304
                $counter++;
305
            }   $counter--;
306
        }
307
 
308
        elsif ($ARGS[$counter] =~ /^-m$/) {                  ## Message ##
309
            $counter++;
310
            $message = "";
311
            while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
312
                if ($message) { $message .= " "; }
313
                $message .= $ARGS[$counter];
314
                $counter++;
315
            }   $counter--;
316
 
317
            ## Replace '\n' with $CRLF.
318
            ## This allows newlines with messages sent on the command line
319
            $message =~ s/\\n/$CRLF/g;
320
        }
321
 
322
        elsif ($ARGS[$counter] =~ /^-u$/) {                  ## Subject ##
323
            $counter++;
324
            $subject = "";
325
            while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
326
                if ($subject) { $subject .= " "; }
327
                $subject .= $ARGS[$counter];
328
                $counter++;
329
            }   $counter--;
330
        }
331
 
332
        elsif ($ARGS[$counter] =~ /^-s$/) {                  ## Server ##
333
            $counter++;
334
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
335
                $conf{'server'} = $ARGS[$counter];
336
                if ($conf{'server'} =~ /:/) {                ## Port ##
337
                    ($conf{'server'},$conf{'port'}) = split(":",$conf{'server'});
338
                }
339
            }
340
            else { printmsg("WARNING - The argument after -s was not the server!", 0); $counter--; }
341
        }
342
 
343
        elsif ($ARGS[$counter] =~ /^-b$/) {                  ## Bind Address ##
344
            $counter++;
345
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
346
                $conf{'bindaddr'} = $ARGS[$counter];
347
            }
348
            else { printmsg("WARNING - The argument after -b was not the bindaddr!", 0); $counter--; }
349
        }
350
 
351
        elsif ($ARGS[$counter] =~ /^-a$/) {                  ## Attachments ##
352
            $counter++;
353
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
354
                push (@attachments,$ARGS[$counter]);
355
                $counter++;
356
            }   $counter--;
357
        }
358
 
359
        elsif ($ARGS[$counter] =~ /^-xu$/) {                  ## AuthSMTP Username ##
360
            $counter++;
361
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
362
               $opt{'username'} = $ARGS[$counter];
363
            }
364
            else {
365
                printmsg("WARNING => The argument after -xu was not valid username!", 0);
366
                $counter--;
367
            }
368
        }
369
 
370
        elsif ($ARGS[$counter] =~ /^-xp$/) {                  ## AuthSMTP Password ##
371
            $counter++;
372
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
373
               $opt{'password'} = $ARGS[$counter];
374
            }
375
            else {
376
                printmsg("WARNING => The argument after -xp was not valid password!", 0);
377
                $counter--;
378
            }
379
        }
380
 
381
        elsif ($ARGS[$counter] =~ /^-l$/) {                  ## Logging ##
382
            $counter++;
383
            $conf{'logging'} = 1;
384
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'logFile'} = $ARGS[$counter]; }
385
            else { printmsg("WARNING - The argument after -l was not the log file!", 0); $counter--; }
386
        }
387
 
388
        elsif ($ARGS[$counter] =~ s/^-v+//i) {               ## Verbosity ##
389
            my $tmp = (length($&) - 1);
390
            $conf{'debug'} += $tmp;
391
        }
392
 
393
        elsif ($ARGS[$counter] =~ /^-q$/) {                  ## Quiet ##
394
            $conf{'stdout'} = 0;
395
        }
396
 
397
        else {
398
            printmsg("Error: \"$ARGS[$counter]\" is not a recognized option!", 0);
399
            help();
400
        }
401
 
402
    }
403
 
404
 
405
 
406
 
407
 
408
 
409
 
410
 
411
    ###################################################
412
    ##  Verify required variables are set correctly  ##
413
    ###################################################
414
 
415
    ## Make sure we have something in $conf{hostname} and $opt{fqdn}
416
    if ($opt{'fqdn'} =~ /\./) {
417
        $conf{'hostname'} = $opt{'fqdn'};
418
        $conf{'hostname'} =~ s/\..*//;
419
    }
420
 
421
    if (!$conf{'server'}) { $conf{'server'} = 'localhost'; }
422
    if (!$conf{'port'})   { $conf{'port'} = 25; }
423
    if (!$from) {
424
        quit("ERROR => You must specify a 'from' field!  Try --help.", 1);
425
    }
426
    if ( ((scalar(@to)) + (scalar(@cc)) + (scalar(@bcc))) <= 0) {
427
        quit("ERROR => You must specify at least one recipient via -t, -cc, or -bcc", 1);
428
    }
429
 
430
    ## Make sure email addresses look OK.
431
    foreach my $addr (@to, @cc, @bcc, $from, $opt{'reply-to'}) {
432
        if ($addr) {
433
            if (!returnAddressParts($addr)) {
434
                printmsg("ERROR => Can't use improperly formatted email address: $addr", 0);
435
                printmsg("HINT => Try viewing the extended help on addressing with \"--help addressing\"", 1);
436
                quit("", 1);
437
            }
438
        }
439
    }
440
 
441
    ## Make sure all attachments exist.
442
    foreach my $file (@attachments) {
443
        if ( (! -f $file) or (! -r $file) ) {
444
            printmsg("ERROR => The attachment [$file] doesn't exist!", 0);
445
            printmsg("HINT => Try specifying the full path to the file or reading extended help with \"--help message\"", 1);
446
            quit("", 1);
447
        }
448
    }
449
 
450
    if ($conf{'logging'} and (!$conf{'logFile'})) {
451
        quit("ERROR => You used -l to enable logging but didn't specify a log file!", 1);
452
    }    
453
 
454
    if ( $opt{'username'} ) {
455
        if (!$opt{'password'}) {
456
            ## Prompt for a password since one wasn't specified with the -xp option.
457
            $SIG{'ALRM'} = sub { quit("ERROR => Timeout waiting for password inpupt", 1); };
458
            alarm(60) if ($^O !~ /win/i);  ## alarm() doesn't work in win32
459
            print "Password: ";
460
            $opt{'password'} = <STDIN>; chomp $opt{'password'};
461
            if (!$opt{'password'}) {
462
                quit("ERROR => A username for SMTP authentication was specified, but no password!", 1);
463
            }
464
        }
465
    }
466
 
467
    ## Validate the TLS setting
468
    $opt{'tls'} = lc($opt{'tls'});
469
    if ($opt{'tls'} !~ /^(auto|yes|no)$/) {
470
        quit("ERROR => Invalid TLS setting ($opt{'tls'}). Must be one of auto, yes, or no.", 1);
471
    }
472
 
473
    ## If TLS is set to "yes", make sure sendEmail loaded the libraries needed.
474
    if ($opt{'tls'} eq 'yes' and $conf{'tls_client'} == 0) {
475
        quit("ERROR => No TLS support!  SendEmail can't load required libraries. (try installing Net::SSLeay and IO::Socket::SSL)", 1);
476
    }
477
 
478
    ## Return 0 errors
479
    return(0);
480
}
481
 
482
 
483
 
484
 
485
 
486
 
487
 
488
 
489
 
490
 
491
 
492
 
493
 
494
 
495
 
496
 
497
## getline($socketRef)
498
sub getline {
499
    my ($socketRef) = @_;
500
    local ($/) = "\r\n";
501
    return $$socketRef->getline;
502
}
503
 
504
 
505
 
506
 
507
## Receive a (multiline?) SMTP response from ($socketRef)
508
sub getResponse {
509
    my ($socketRef) = @_;
510
    my ($tmp, $reply);
511
    local ($/) = "\r\n";
512
    return undef unless defined($tmp = getline($socketRef));
513
    return("getResponse() socket is not open") unless ($$socketRef->opened);
514
    ## Keep reading lines if it's a multi-line response
515
    while ($tmp =~ /^\d{3}-/o) {
516
        $reply .= $tmp;
517
        return undef unless defined($tmp = getline($socketRef));
518
    }
519
    $reply .= $tmp;
520
    $reply =~ s/\r?\n$//o;
521
    return $reply;
522
}
523
 
524
 
525
 
526
 
527
###############################################################################################
528
##  Function:    SMTPchat ( [string $command] )
529
##
530
##  Description: Sends $command to the SMTP server (on SERVER) and awaits a successful
531
##               reply form the server.  If the server returns an error, or does not reply
532
##               within $conf{'alarm'} seconds an error is generated.
533
##               NOTE: $command is optional, if no command is specified then nothing will
534
##               be sent to the server, but a valid response is still required from the server.
535
##
536
##  Input:       [$command]          A (optional) valid SMTP command (ex. "HELO")
537
##  
538
##  
539
##  Output:      Returns zero on success, or non-zero on error.  
540
##               Error messages will be stored in $conf{'error'}
541
##               A copy of the last SMTP response is stored in the global variable
542
##               $conf{'SMTPchat_response'}
543
##               
544
##  
545
##  Example:     SMTPchat ("HELO mail.isp.net");
546
###############################################################################################
547
sub SMTPchat {
548
    my ($command) = @_;
549
 
550
    printmsg("INFO => Sending: \t$command", 1) if ($command);
551
 
552
    ## Send our command
553
    print $SERVER "$command$CRLF" if ($command);
554
 
555
    ## Read a response from the server
556
    $SIG{'ALRM'} = sub { $conf{'error'} = "alarm"; $SERVER->close(); };
557
    alarm($conf{'alarm'}) if ($^O !~ /win/i);  ## alarm() doesn't work in win32;
558
    my $result = $conf{'SMTPchat_response'} = getResponse(\$SERVER); 
559
    alarm(0) if ($^O !~ /win/i);  ## alarm() doesn't work in win32;
560
 
561
    ## Generate an alert if we timed out
562
    if ($conf{'error'} eq "alarm") {
563
        $conf{'error'} = "ERROR => Timeout while reading from $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds.";
564
        return(1);
565
    }
566
 
567
    ## Make sure the server actually responded
568
    if (!$result) {
569
        $conf{'error'} = "ERROR => $conf{'server'}:$conf{'port'} returned a zero byte response to our query.";
570
        return(2);
571
    }
572
 
573
    ## Validate the response
574
    if (evalSMTPresponse($result)) {
575
        ## conf{'error'} will already be set here
576
        return(2);
577
    }
578
 
579
    ## Print the success messsage
580
    printmsg($conf{'error'}, 1);
581
 
582
    ## Return Success
583
    return(0);
584
}
585
 
586
 
587
 
588
 
589
 
590
 
591
 
592
 
593
 
594
 
595
 
596
 
597
###############################################################################################
598
##  Function:    evalSMTPresponse (string $message )
599
##
600
##  Description: Searches $message for either an  SMTP success or error code, and returns
601
##               0 on success, and the actual error code on error.
602
##               
603
##
604
##  Input:       $message          Data received from a SMTP server (ex. "220 
605
##                                
606
##  
607
##  Output:      Returns zero on success, or non-zero on error.  
608
##               Error messages will be stored in $conf{'error'}
609
##               
610
##  
611
##  Example:     SMTPchat ("HELO mail.isp.net");
612
###############################################################################################
613
sub evalSMTPresponse {
614
    my ($message) = @_;
615
 
616
    ## Validate input
617
    if (!$message) { 
618
        $conf{'error'} = "ERROR => No message was passed to evalSMTPresponse().  What happened?";
619
        return(1)
620
    }
621
 
622
    printmsg("DEBUG => evalSMTPresponse() - Checking for SMTP success or error status in the message: $message ", 3);
623
 
624
    ## Look for a SMTP success code
625
    if ($message =~ /^([23]\d\d)/) {
626
        printmsg("DEBUG => evalSMTPresponse() - Found SMTP success code: $1", 2);
627
        $conf{'error'} = "SUCCESS => Received: \t$message";
628
        return(0);
629
    }
630
 
631
    ## Look for a SMTP error code
632
    if ($message =~ /^([45]\d\d)/) {
633
        printmsg("DEBUG => evalSMTPresponse() - Found SMTP error code: $1", 2);
634
        $conf{'error'} = "ERROR => Received: \t$message";
635
        return($1);
636
    }
637
 
638
    ## If no SMTP codes were found return an error of 1
639
    $conf{'error'} = "ERROR => Received a message with no success or error code. The message received was: $message";
640
    return(2);
641
 
642
}
643
 
644
 
645
 
646
 
647
 
648
 
649
 
650
 
651
 
652
 
653
#########################################################
654
# SUB: &return_month(0,1,etc)
655
#  returns the name of the month that corrosponds
656
#  with the number.  returns 0 on error.
657
#########################################################
658
sub return_month {
659
    my $x = $_[0];
660
    if ($x == 0)  { return 'Jan'; }
661
    if ($x == 1)  { return 'Feb'; }
662
    if ($x == 2)  { return 'Mar'; }
663
    if ($x == 3)  { return 'Apr'; }
664
    if ($x == 4)  { return 'May'; }
665
    if ($x == 5)  { return 'Jun'; }
666
    if ($x == 6)  { return 'Jul'; }
667
    if ($x == 7)  { return 'Aug'; }
668
    if ($x == 8)  { return 'Sep'; }
669
    if ($x == 9)  { return 'Oct'; }
670
    if ($x == 10) { return 'Nov'; }
671
    if ($x == 11) { return 'Dec'; }
672
    return (0);
673
}
674
 
675
 
676
 
677
 
678
 
679
 
680
 
681
 
682
 
683
 
684
 
685
 
686
 
687
 
688
 
689
 
690
#########################################################
691
# SUB: &return_day(0,1,etc)
692
#  returns the name of the day that corrosponds
693
#  with the number.  returns 0 on error.
694
#########################################################
695
sub return_day {
696
    my $x = $_[0];
697
    if ($x == 0)  { return 'Sun'; }
698
    if ($x == 1)  { return 'Mon'; }
699
    if ($x == 2)  { return 'Tue'; }
700
    if ($x == 3)  { return 'Wed'; }
701
    if ($x == 4)  { return 'Thu'; }
702
    if ($x == 5)  { return 'Fri'; }
703
    if ($x == 6)  { return 'Sat'; }
704
    return (0);
705
}
706
 
707
 
708
 
709
 
710
 
711
 
712
 
713
 
714
 
715
 
716
 
717
 
718
 
719
 
720
 
721
 
722
###############################################################################################
723
##  Function:    returnAddressParts(string $address)
724
##
725
##  Description: Returns a two element array containing the "Name" and "Address" parts of 
726
##               an email address.
727
##  
728
## Example:      "Brandon Zehm <caspian@dotconf.net>"
729
##               would return: ("Brandon Zehm", "caspian@dotconf.net");
730
## 
731
##               "caspian@dotconf.net"
732
##               would return: ("caspian@dotconf.net", "caspian@dotconf.net")
733
###############################################################################################
734
sub returnAddressParts {
735
    my $input = $_[0];
736
    my $name = "";
737
    my $address = "";
738
 
739
    ## Make sure to fail if it looks totally invalid
740
    if ($input !~ /(\S+\@\S+)/) {
741
        $conf{'error'} = "ERROR => The address [$input] doesn't look like a valid email address, ignoring it";
742
        return(undef());
743
    }
744
 
745
    ## Check 1, should find addresses like: "Brandon Zehm <caspian@dotconf.net>"
746
    elsif ($input =~ /^\s*(\S(.*\S)?)\s*<(\S+\@\S+)>/o) {
747
        ($name, $address) = ($1, $3);
748
    }
749
 
750
    ## Otherwise if that failed, just get the address: <caspian@dotconf.net>
751
    elsif ($input =~ /<(\S+\@\S+)>/o) {
752
        $name = $address = $1;
753
    }
754
 
755
    ## Or maybe it was formatted this way: caspian@dotconf.net
756
    elsif ($input =~ /(\S+\@\S+)/o) {
757
        $name = $address = $1;
758
    }
759
 
760
    ## Something stupid happened, just return an error.
761
    unless ($name and $address) {
762
        printmsg("ERROR => Couldn't parse the address: $input", 0);
763
        printmsg("HINT => If you think this should work, consider reporting this as a bug to $conf{'authorEmail'}", 1);
764
        return(undef());
765
    }
766
 
767
    ## Make sure there aren't invalid characters in the address, and return it.
768
    my $ctrl        = '\000-\037';
769
    my $nonASCII    = '\x80-\xff';
770
    if ($address =~ /[<> ,;:"'\[\]\\$ctrl$nonASCII]/) {
771
        printmsg("WARNING => The address [$address] seems to contain invalid characters: continuing anyway", 0);
772
    }
773
    return($name, $address);
774
}
775
 
776
 
777
 
778
 
779
 
780
 
781
 
782
 
783
 
784
 
785
 
786
 
787
 
788
 
789
 
790
 
791
###############################################################################################
792
##  Function:    base64_encode(string $data, bool $chunk)
793
##
794
##  Description: Returns $data as a base64 encoded string.
795
##               If $chunk is true, the encoded data is returned in 76 character long lines
796
##               with the final \CR\LF removed.
797
##
798
##  Note: This is only used from the smtp auth section of code.
799
##        At some point it would be nice to merge the code that encodes attachments and this.
800
###############################################################################################
801
sub base64_encode {
802
    my $data = $_[0];
803
    my $chunk = $_[1];
804
    my $tmp = '';
805
    my $base64 = '';
806
    my $CRLF = "\r\n";
807
 
808
    ###################################
809
    ## Convert binary data to base64 ##
810
    ###################################
811
    while ($data =~ s/(.{45})//s) {        ## Get 45 bytes from the binary string
812
        $tmp = substr(pack('u', $&), 1);   ## Convert the binary to uuencoded text
813
        chop($tmp);
814
        $tmp =~ tr|` -_|AA-Za-z0-9+/|;     ## Translate from uuencode to base64
815
        $base64 .= $tmp;
816
    }
817
 
818
    ##########################
819
    ## Encode the leftovers ##
820
    ##########################
821
    my $padding = "";
822
    if ( ($data) and (length($data) > 0) ) {
823
        $padding = (3 - length($data) % 3) % 3;    ## Set flag if binary data isn't divisible by 3
824
        $tmp = substr(pack('u', $data), 1);        ## Convert the binary to uuencoded text
825
        chop($tmp);
826
        $tmp =~ tr|` -_|AA-Za-z0-9+/|;             ## Translate from uuencode to base64
827
        $base64 .= $tmp;
828
    }
829
 
830
    ############################
831
    ## Fix padding at the end ##
832
    ############################
833
    $data = '';
834
    $base64 =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set
835
    if ($chunk) {
836
        while ($base64 =~ s/(.{1,76})//s) {                     ## Put $CRLF after each 76 characters
837
            $data .= "$1$CRLF";
838
        }
839
    }
840
    else {
841
        $data = $base64;
842
    }
843
 
844
    ## Remove any trailing CRLF's
845
    $data =~ s/(\r|\n)*$//s;
846
    return($data);
847
}
848
 
849
 
850
 
851
 
852
 
853
 
854
 
855
 
856
 
857
#########################################################
858
# SUB: send_attachment("/path/filename")
859
# Sends the mime headers and base64 encoded file
860
# to the email server.
861
#########################################################
862
sub send_attachment {
863
    my ($filename) = @_;                             ## Get filename passed
864
    my (@fields, $y, $filename_name, $encoding,      ## Local variables
865
        @attachlines, $content_type);
866
    my $bin = 1;
867
 
868
    @fields = split(/\/|\\/, $filename);             ## Get the actual filename without the path  
869
    $filename_name = pop(@fields);       
870
    push @attachments_names, $filename_name;         ## FIXME: This is only used later for putting in the log file
871
 
872
    ##########################
873
    ## Autodetect Mime Type ##
874
    ##########################
875
 
876
    @fields = split(/\./, $filename_name);
877
    $encoding = $fields[$#fields];
878
 
879
    if ($encoding =~ /txt|text|log|conf|^c$|cpp|^h$|inc|m3u/i) {   $content_type = 'text/plain';                      }
880
    elsif ($encoding =~ /html|htm|shtml|shtm|asp|php|cfm/i) {      $content_type = 'text/html';                       }
881
    elsif ($encoding =~ /sh$/i) {                                  $content_type = 'application/x-sh';                }
882
    elsif ($encoding =~ /tcl/i) {                                  $content_type = 'application/x-tcl';               }
883
    elsif ($encoding =~ /pl$/i) {                                  $content_type = 'application/x-perl';              }
884
    elsif ($encoding =~ /js$/i) {                                  $content_type = 'application/x-javascript';        }
885
    elsif ($encoding =~ /man/i) {                                  $content_type = 'application/x-troff-man';         }
886
    elsif ($encoding =~ /gif/i) {                                  $content_type = 'image/gif';                       }
887
    elsif ($encoding =~ /jpg|jpeg|jpe|jfif|pjpeg|pjp/i) {          $content_type = 'image/jpeg';                      }
888
    elsif ($encoding =~ /tif|tiff/i) {                             $content_type = 'image/tiff';                      }
889
    elsif ($encoding =~ /xpm/i) {                                  $content_type = 'image/x-xpixmap';                 }
890
    elsif ($encoding =~ /bmp/i) {                                  $content_type = 'image/x-MS-bmp';                  }
891
    elsif ($encoding =~ /pcd/i) {                                  $content_type = 'image/x-photo-cd';                }
892
    elsif ($encoding =~ /png/i) {                                  $content_type = 'image/png';                       }
893
    elsif ($encoding =~ /aif|aiff/i) {                             $content_type = 'audio/x-aiff';                    }
894
    elsif ($encoding =~ /wav/i) {                                  $content_type = 'audio/x-wav';                     }
895
    elsif ($encoding =~ /mp2|mp3|mpa/i) {                          $content_type = 'audio/x-mpeg';                    }
896
    elsif ($encoding =~ /ra$|ram/i) {                              $content_type = 'audio/x-pn-realaudio';            }
897
    elsif ($encoding =~ /mpeg|mpg/i) {                             $content_type = 'video/mpeg';                      }
898
    elsif ($encoding =~ /mov|qt$/i) {                              $content_type = 'video/quicktime';                 }
899
    elsif ($encoding =~ /avi/i) {                                  $content_type = 'video/x-msvideo';                 }
900
    elsif ($encoding =~ /zip/i) {                                  $content_type = 'application/x-zip-compressed';    }
901
    elsif ($encoding =~ /tar/i) {                                  $content_type = 'application/x-tar';               }
902
    elsif ($encoding =~ /jar/i) {                                  $content_type = 'application/java-archive';        }
903
    elsif ($encoding =~ /exe|bin/i) {                              $content_type = 'application/octet-stream';        }
904
    elsif ($encoding =~ /ppt|pot|ppa|pps|pwz/i) {                  $content_type = 'application/vnd.ms-powerpoint';   }
905
    elsif ($encoding =~ /mdb|mda|mde/i) {                          $content_type = 'application/vnd.ms-access';       }
906
    elsif ($encoding =~ /xls|xlt|xlm|xld|xla|xlc|xlw|xll/i) {      $content_type = 'application/vnd.ms-excel';        }
907
    elsif ($encoding =~ /doc|dot/i) {                              $content_type = 'application/msword';              }
908
    elsif ($encoding =~ /rtf/i) {                                  $content_type = 'application/rtf';                 }
909
    elsif ($encoding =~ /pdf/i) {                                  $content_type = 'application/pdf';                 }
910
    elsif ($encoding =~ /tex/i) {                                  $content_type = 'application/x-tex';               }
911
    elsif ($encoding =~ /latex/i) {                                $content_type = 'application/x-latex';             }
912
    elsif ($encoding =~ /vcf/i) {                                  $content_type = 'application/x-vcard';             }
913
    else { $content_type = 'application/octet-stream';  }
914
 
915
 
916
  ############################
917
  ## Process the attachment ##
918
  ############################
919
 
920
    #####################################
921
    ## Generate and print MIME headers ##
922
    #####################################
923
 
924
    $y  = "$CRLF--$conf{'delimiter'}$CRLF";
925
    $y .= "Content-Type: $content_type;$CRLF";
926
    $y .= "        name=\"$filename_name\"$CRLF";
927
    $y .= "Content-Transfer-Encoding: base64$CRLF";
928
    $y .= "Content-Disposition: attachment; filename=\"$filename_name\"$CRLF";
929
    $y .= "$CRLF";
930
    print $SERVER $y;
931
 
932
 
933
    ###########################################################
934
    ## Convert the file to base64 and print it to the server ##
935
    ###########################################################
936
 
937
    open (FILETOATTACH, $filename) || do {
938
        printmsg("ERROR => Opening the file [$filename] for attachment failed with the error: $!", 0);
939
        return(1);
940
    };
941
    binmode(FILETOATTACH);                 ## Hack to make Win32 work
942
 
943
    my $res = "";
944
    my $tmp = "";
945
    my $base64 = "";
946
    while (<FILETOATTACH>) {               ## Read a line from the (binary) file
947
        $res .= $_;
948
 
949
        ###################################
950
        ## Convert binary data to base64 ##
951
        ###################################
952
        while ($res =~ s/(.{45})//s) {         ## Get 45 bytes from the binary string
953
            $tmp = substr(pack('u', $&), 1);   ## Convert the binary to uuencoded text
954
            chop($tmp);
955
            $tmp =~ tr|` -_|AA-Za-z0-9+/|;     ## Translate from uuencode to base64
956
            $base64 .= $tmp;
957
        }
958
 
959
        ################################
960
        ## Print chunks to the server ##
961
        ################################
962
        while ($base64 =~ s/(.{76})//s) {
963
            print $SERVER "$1$CRLF";
964
        }
965
 
966
    }
967
 
968
    ###################################
969
    ## Encode and send the leftovers ##
970
    ###################################
971
    my $padding = "";
972
    if ( ($res) and (length($res) >= 1) ) {
973
        $padding = (3 - length($res) % 3) % 3;  ## Set flag if binary data isn't divisible by 3
974
        $res = substr(pack('u', $res), 1);      ## Convert the binary to uuencoded text
975
        chop($res);
976
        $res =~ tr|` -_|AA-Za-z0-9+/|;          ## Translate from uuencode to base64
977
    }
978
 
979
    ############################
980
    ## Fix padding at the end ##
981
    ############################
982
    $res = $base64 . $res;                               ## Get left overs from above
983
    $res =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set
984
    if ($res) {
985
        while ($res =~ s/(.{1,76})//s) {                 ## Send it to the email server.
986
            print $SERVER "$1$CRLF";
987
        }
988
    }
989
 
990
    close (FILETOATTACH) || do {
991
        printmsg("ERROR - Closing the filehandle for file [$filename] failed with the error: $!", 0);
992
        return(2);
993
    };
994
 
995
    ## Return 0 errors
996
    return(0);
997
 
998
}
999
 
1000
 
1001
 
1002
 
1003
 
1004
 
1005
 
1006
 
1007
 
1008
###############################################################################################
1009
##  Function:    $string = get_hostname (boot $fqdn)
1010
##  
1011
##  Description: Tries really hard to returns the short (or FQDN) hostname of the current
1012
##               system.  Uses techniques and code from the  Sys-Hostname module.
1013
##  
1014
##  Input:       $fqdn     A true value (1) will cause this function to return a FQDN hostname
1015
##                         rather than a short hostname.
1016
##  
1017
##  Output:      Returns a string
1018
###############################################################################################
1019
sub get_hostname {
1020
    ## Assign incoming parameters to variables
1021
    my ( $fqdn ) = @_;
1022
    my $hostname = "";
1023
 
1024
    ## STEP 1: Get short hostname
1025
 
1026
    ## Load Sys::Hostname if it's available
1027
    eval { require Sys::Hostname; };
1028
    unless ($@) {
1029
        $hostname = Sys::Hostname::hostname(); 
1030
    }
1031
 
1032
    ## If that didn't get us a hostname, try a few other things
1033
    else {
1034
        ## Windows systems
1035
        if ($^O !~ /win/i) {
1036
            if ($ENV{'COMPUTERNAME'}) { $hostname = $ENV{'COMPUTERNAME'}; }
1037
            if (!$hostname) { $hostname = gethostbyname('localhost'); }
1038
            if (!$hostname) { chomp($hostname = `hostname 2> NUL`) };
1039
        }
1040
 
1041
        ## Unix systems
1042
        else {
1043
            local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin';  ## Paranoia
1044
 
1045
            ## Try the environment first (Help!  What other variables could/should I be checking here?)
1046
            if ($ENV{'HOSTNAME'}) { $hostname = $ENV{'HOSTNAME'}; }
1047
 
1048
            ## Try the hostname command
1049
            eval { local $SIG{__DIE__}; local $SIG{CHLD}; $hostname = `hostname 2>/dev/null`; chomp($hostname); } ||
1050
 
1051
            ## Try POSIX::uname(), which strictly can't be expected to be correct
1052
            eval { local $SIG{__DIE__}; require POSIX; $hostname = (POSIX::uname())[1]; } ||
1053
 
1054
            ## Try the uname command
1055
            eval { local $SIG{__DIE__}; $hostname = `uname -n 2>/dev/null`; chomp($hostname); };
1056
 
1057
        }
1058
 
1059
        ## If we can't find anything else, return ""
1060
        if (!$hostname) {
1061
            print "WARNING => No hostname could be determined, please specify one with -o fqdn=FQDN option!\n";
1062
            return("unknown");
1063
        }
1064
    }
1065
 
1066
    ## Return the short hostname
1067
    unless ($fqdn) {
1068
        $hostname =~ s/\..*//;
1069
        return(lc($hostname));
1070
    }
1071
 
1072
    ## STEP 2: Determine the FQDN
1073
 
1074
    ## First, if we already have one return it.
1075
    if ($hostname =~ /\w\.\w/) { return(lc($hostname)); }
1076
 
1077
    ## Next try using 
1078
    eval { $fqdn = (gethostbyname($hostname))[0]; };
1079
    if ($fqdn) { return(lc($fqdn)); }
1080
    return(lc($hostname));
1081
}
1082
 
1083
 
1084
 
1085
 
1086
 
1087
 
1088
 
1089
 
1090
###############################################################################################
1091
##  Function:    printmsg (string $message, int $level)
1092
##
1093
##  Description: Handles all messages - printing them to the screen only if the messages
1094
##               $level is >= the global debug level.  If $conf{'logFile'} is defined it
1095
##               will also log the message to that file.
1096
##
1097
##  Input:       $message          A message to be printed, logged, etc.
1098
##               $level            The debug level of the message. If
1099
##                                 not defined 0 will be assumed.  0 is
1100
##                                 considered a normal message, 1 and 
1101
##                                 higher is considered a debug message.
1102
##  
1103
##  Output:      Prints to STDOUT
1104
##
1105
##  Assumptions: $conf{'hostname'} should be the name of the computer we're running on.
1106
##               $conf{'stdout'} should be set to 1 if you want to print to stdout
1107
##               $conf{'logFile'} should be a full path to a log file if you want that
1108
##               $conf{'debug'} should be an integer between 0 and 10.
1109
##
1110
##  Example:     printmsg("WARNING: We believe in generic error messages... NOT!", 0);
1111
###############################################################################################
1112
sub printmsg {
1113
    ## Assign incoming parameters to variables
1114
    my ( $message, $level ) = @_;
1115
 
1116
    ## Make sure input is sane
1117
    $level = 0 if (!defined($level));
1118
    $message =~ s/\s+$//sgo;
1119
    $message =~ s/\r?\n/, /sgo;
1120
 
1121
    ## Continue only if the debug level of the program is >= message debug level.
1122
    if ($conf{'debug'} >= $level) {
1123
 
1124
        ## Get the date in the format: Dec  3 11:14:04
1125
        my ($sec, $min, $hour, $mday, $mon) = localtime();
1126
        $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon];
1127
        my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec);
1128
 
1129
        ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true.
1130
        if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) {
1131
            print "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n";
1132
        }
1133
 
1134
        ## Print to the log file if $conf{'logging'} is true
1135
        if ($conf{'logFile'}) {
1136
            if (openLogFile($conf{'logFile'})) { $conf{'logFile'} = ""; printmsg("ERROR => Opening the file [$conf{'logFile'}] for appending returned the error: $!", 1); }
1137
            print LOGFILE "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n";
1138
        }
1139
 
1140
    }
1141
 
1142
    ## Return 0 errors
1143
    return(0);
1144
}
1145
 
1146
 
1147
 
1148
 
1149
 
1150
 
1151
 
1152
 
1153
 
1154
 
1155
 
1156
 
1157
###############################################################################################
1158
## FUNCTION:
1159
##   openLogFile ( $filename )
1160
## 
1161
## 
1162
## DESCRIPTION: 
1163
##   Opens the file $filename and attaches it to the filehandle "LOGFILE".  Returns 0 on success
1164
##   and non-zero on failure.  Error codes are listed below, and the error message gets set in
1165
##   global variable $!.
1166
##   
1167
##   
1168
## Example:
1169
##   openFile ("/var/log/sendEmail.log");
1170
##
1171
###############################################################################################
1172
sub openLogFile {
1173
    ## Get the incoming filename
1174
    my $filename = $_[0];
1175
 
1176
    ## Make sure our file exists, and if the file doesn't exist then create it
1177
    if ( ! -f $filename ) {
1178
        print STDERR "NOTICE: The log file [$filename] does not exist.  Creating it now with mode [0600].\n" if ($conf{'stdout'});
1179
        open (LOGFILE, ">>$filename");
1180
        close LOGFILE;
1181
        chmod (0600, $filename);
1182
    }
1183
 
1184
    ## Now open the file and attach it to a filehandle
1185
    open (LOGFILE,">>$filename") or return (1);
1186
 
1187
    ## Put the file into non-buffering mode
1188
    select LOGFILE;
1189
    $| = 1;
1190
    select STDOUT;
1191
 
1192
    ## Return success
1193
    return(0);
1194
}
1195
 
1196
 
1197
 
1198
 
1199
 
1200
 
1201
 
1202
 
1203
###############################################################################################
1204
##  Function:    read_file (string $filename)
1205
##  
1206
##  Description: Reads the contents of a file and returns a two part array:
1207
##               ($status, $file-contents)
1208
##               $status is 0 on success, non-zero on error.
1209
##               
1210
##  Example:     ($status, $file) = read_file("/etc/passwd");
1211
###############################################################################################
1212
sub read_file {
1213
    my ( $filename ) = @_;
1214
 
1215
    ## If the value specified is a file, load the file's contents
1216
    if ( (-e $filename and -r $filename) ) {
1217
        my $FILE;
1218
        if(!open($FILE, ' ' . $filename)) {
1219
            return((1, ""));
1220
        }
1221
        my $file = '';
1222
        while (<$FILE>) {
1223
            $file .= $_;
1224
        }
1225
        ## Strip an ending \r\n
1226
        $file =~ s/\r?\n$//os;
1227
    }
1228
    return((1, ""));
1229
}
1230
 
1231
 
1232
 
1233
 
1234
 
1235
 
1236
 
1237
 
1238
 
1239
###############################################################################################
1240
##  Function:    quit (string $message, int $errorLevel)
1241
##  
1242
##  Description: Exits the program, optionally printing $message.  It 
1243
##               returns an exit error level of $errorLevel to the 
1244
##               system  (0 means no errors, and is assumed if empty.)
1245
##
1246
##  Example:     quit("Exiting program normally", 0);
1247
###############################################################################################
1248
sub quit {
1249
    my ( $message, $errorLevel ) = @_;
1250
    $errorLevel = 0 if (!defined($errorLevel));
1251
 
1252
    ## Print exit message
1253
    if ($message) { 
1254
        printmsg($message, 0);
1255
    }
1256
 
1257
    ## Exit
1258
    exit($errorLevel);
1259
}
1260
 
1261
 
1262
 
1263
 
1264
 
1265
 
1266
 
1267
 
1268
 
1269
 
1270
 
1271
 
1272
###############################################################################################
1273
## Function:    help ()
1274
##
1275
## Description: For all those newbies ;)
1276
##              Prints a help message and exits the program.
1277
##
1278
###############################################################################################
1279
sub help {
1280
exit(1) if (!$conf{'stdout'});
1281
print <<EOM;
1282
 
1283
${colorBold}$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>${colorNoBold}
1284
 
1285
Synopsis:  $conf{'programName'} -f ADDRESS [options]
1286
 
1287
  ${colorRed}Required:${colorNormal}
1288
    -f ADDRESS                from (sender) email address
1289
    * At least one recipient required via -t, -cc, or -bcc
1290
    * Message body required via -m, STDIN, or -o message-file=FILE
1291
 
1292
  ${colorGreen}Common:${colorNormal}
1293
    -t ADDRESS [ADDR ...]     to email address(es)
1294
    -u SUBJECT                message subject
1295
    -m MESSAGE                message body
1296
    -s SERVER[:PORT]          smtp mail relay, default is $conf{'server'}:$conf{'port'}
1297
 
1298
  ${colorGreen}Optional:${colorNormal}
1299
    -a   FILE [FILE ...]      file attachment(s)
1300
    -cc  ADDRESS [ADDR ...]   cc  email address(es)
1301
    -bcc ADDRESS [ADDR ...]   bcc email address(es)
1302
    -xu  USERNAME             username for SMTP authentication
1303
    -xp  PASSWORD             password for SMTP authentication
1304
 
1305
  ${colorGreen}Paranormal:${colorNormal}
1306
    -b BINDADDR[:PORT]        local host bind address
1307
    -l LOGFILE                log to the specified file
1308
    -v                        verbosity, use multiple times for greater effect
1309
    -q                        be quiet (i.e. no STDOUT output)
1310
    -o NAME=VALUE             advanced options, for details try: --help misc
1311
        -o message-content-type=<auto|text|html>
1312
        -o message-file=FILE         -o message-format=raw
1313
        -o message-header=HEADER     -o message-charset=CHARSET
1314
        -o reply-to=ADDRESS          -o timeout=SECONDS
1315
        -o username=USERNAME         -o password=PASSWORD
1316
        -o tls=<auto|yes|no>         -o fqdn=FQDN
1317
 
1318
 
1319
  ${colorGreen}Help:${colorNormal}
1320
    --help                    the helpful overview you're reading now
1321
    --help addressing         explain addressing and related options
1322
    --help message            explain message body input and related options
1323
    --help networking         explain -s, -b, etc
1324
    --help output             explain logging and other output options
1325
    --help misc               explain -o options, TLS, SMTP auth, and more
1326
 
1327
EOM
1328
exit(1);
1329
}
1330
 
1331
 
1332
 
1333
 
1334
 
1335
 
1336
 
1337
 
1338
 
1339
###############################################################################################
1340
## Function:    helpTopic ($topic)
1341
##
1342
## Description: For all those newbies ;) 
1343
##              Prints a help message and exits the program.
1344
## 
1345
###############################################################################################
1346
sub helpTopic {
1347
    exit(1) if (!$conf{'stdout'});
1348
    my ($topic) = @_;
1349
 
1350
    CASE: {
1351
 
1352
 
1353
 
1354
 
1355
## ADDRESSING
1356
        ($topic eq 'addressing') && do {
1357
            print <<EOM;
1358
 
1359
${colorBold}ADDRESSING DOCUMENTATION${colorNormal}
1360
 
1361
${colorGreen}Addressing Options${colorNormal}
1362
Options related to addressing:
1363
    -f   ADDRESS
1364
    -t   ADDRESS [ADDRESS ...]
1365
    -cc  ADDRESS [ADDRESS ...]
1366
    -bcc ADDRESS [ADDRESS ...]
1367
    -o   reply-to=ADDRESS
1368
 
1369
-f ADDRESS
1370
    This required option specifies who the email is from, I.E. the sender's
1371
    email address.
1372
 
1373
-t ADDRESS [ADDRESS ...]
1374
    This option specifies the primary recipient(s).  At least one recipient
1375
    address must be specified via the -t, -cc. or -bcc options.
1376
 
1377
-cc ADDRESS [ADDRESS ...]
1378
    This option specifies the "carbon copy" recipient(s).  At least one 
1379
    recipient address must be specified via the -t, -cc. or -bcc options.
1380
 
1381
-bcc ADDRESS [ADDRESS ...]
1382
    This option specifies the "blind carbon copy" recipient(s).  At least
1383
    one recipient address must be specified via the -t, -cc. or -bcc options.
1384
 
1385
-o reply-to=ADDRESS
1386
    This option specifies that an optional "Reply-To" address should be
1387
    written in the email's headers.
1388
 
1389
 
1390
${colorGreen}Email Address Syntax${colorNormal}
1391
Email addresses may be specified in one of two ways:
1392
    Full Name:     "John Doe <john.doe\@gmail.com>"
1393
    Just Address:  "john.doe\@gmail.com"
1394
 
1395
The "Full Name" method is useful if you want a name, rather than a plain
1396
email address, to be displayed in the recipient's From, To, or Cc fields
1397
when they view the message.
1398
 
1399
 
1400
${colorGreen}Multiple Recipients${colorNormal}
1401
The -t, -cc, and -bcc options each accept multiple addresses.  They may be
1402
specified by separating them by either a white space, comma, or semi-colon
1403
separated list.  You may also specify the -t, -cc, and -bcc options multiple
1404
times, each occurance will append the new recipients to the respective list.
1405
 
1406
Examples:
1407
(I used "-t" in these examples, but it can be "-cc" or "-bcc" as well)
1408
 
1409
  * Space separated list:
1410
    -t jane.doe\@yahoo.com "John Doe <john.doe\@gmail.com>"
1411
 
1412
  * Semi-colon separated list:
1413
    -t "jane.doe\@yahoo.com; John Doe <john.doe\@gmail.com>"
1414
 
1415
  * Comma separated list:
1416
    -t "jane.doe\@yahoo.com, John Doe <john.doe\@gmail.com>"
1417
 
1418
  * Multiple -t, -cc, or -bcc options:
1419
    -t "jane.doe\@yahoo.com" -t "John Doe <john.doe\@gmail.com>"
1420
 
1421
 
1422
EOM
1423
            last CASE;
1424
        };
1425
 
1426
 
1427
 
1428
 
1429
 
1430
 
1431
## MESSAGE
1432
        ($topic eq 'message') && do {
1433
            print <<EOM;
1434
 
1435
${colorBold}MESSAGE DOCUMENTATION${colorNormal}
1436
 
1437
${colorGreen}Message Options${colorNormal}
1438
Options related to the email message body:
1439
    -u  SUBJECT
1440
    -m  MESSAGE
1441
    -o  message-file=FILE
1442
    -o  message-content-type=<auto|text|html>
1443
    -o  message-header=EMAIL HEADER
1444
    -o  message-charset=CHARSET
1445
    -o  message-format=raw
1446
 
1447
-u SUBJECT
1448
    This option allows you to specify the subject for your email message.
1449
    It is not required (anymore) that the subject be quoted, although it 
1450
    is recommended.  The subject will be read until an argument starting
1451
    with a hyphen (-) is found.  
1452
    Examples:
1453
      -u "Contact information while on vacation"
1454
      -u New Microsoft vulnerability discovered
1455
 
1456
-m MESSAGE
1457
    This option is one of three methods that allow you to specify the message
1458
    body for your email.  The message may be specified on the command line
1459
    with this -m option, read from a file with the -o message-file=FILE
1460
    option, or read from STDIN if neither of these options are present.
1461
 
1462
    It is not required (anymore) that the message be quoted, although it is
1463
    recommended.  The message will be read until an argument starting with a
1464
    hyphen (-) is found.
1465
    Examples:
1466
      -m "See you in South Beach, Hawaii.  -Todd"
1467
      -m Please ensure that you upgrade your systems right away
1468
 
1469
    Multi-line message bodies may be specified with the -m option by putting
1470
    a "\\n" into the message.  Example:
1471
      -m "This is line 1.\\nAnd this is line 2."
1472
 
1473
    HTML messages are supported, simply begin your message with "<html>" and
1474
    sendEmail will properly label the mime header so MUAs properly render
1475
    the message.  It is currently not possible without "-o message-format=raw"
1476
    to send a message with both text and html parts with sendEmail.
1477
 
1478
-o message-file=FILE
1479
    This option is one of three methods that allow you to specify the message
1480
    body for your email.  To use this option simply specify a text file
1481
    containing the body of your email message. Examples:
1482
      -o message-file=/root/message.txt
1483
      -o message-file="C:\\Program Files\\output.txt"
1484
 
1485
-o message-content-type=<auto|text|html>
1486
    This option allows you to specify the content-type of the email. If your
1487
    email message is an html message but is being displayed as a text message
1488
    just add "-o message-content-type=html" to the command line to force it
1489
    to display as an html message. This actually just changes the Content-Type:
1490
    header. Advanced users will be happy to know that if you specify anything
1491
    other than the three options listed above it will use that as the vaule
1492
    for the Content-Type header.
1493
 
1494
-o message-header=EMAIL HEADER
1495
    This option allows you to specify additional email headers to be included.
1496
    To add more than one message header simply use this option on the command
1497
    line more than once.  If you specify a message header that sendEmail would
1498
    normally generate the one you specified will be used in it's place.
1499
    Do not use this unless you know what you are doing!
1500
    Example:
1501
      To scare a Microsoft Outlook user you may want to try this:
1502
      -o message-header="X-Message-Flag: Message contains illegal content"
1503
    Example:
1504
      To request a read-receipt try this:
1505
      -o message-header="Disposition-Notification-To: <user\@domain.com>"
1506
    Example:
1507
      To set the message priority try this:
1508
      -o message-header="X-Priority: 1"
1509
      Priority reference: 1=highest, 2=high, 3=normal, 4=low, 5=lowest
1510
 
1511
-o message-charset=CHARSET
1512
    This option allows you to specify the character-set for the message body.
1513
    The default is iso-8859-1.
1514
 
1515
-o message-format=raw
1516
    This option instructs sendEmail to assume the message (specified with -m,
1517
    read from STDIN, or read from the file specified in -o message-file=FILE)
1518
    is already a *complete* email message.  SendEmail will not generate any
1519
    headers and will transmit the message as-is to the remote SMTP server.
1520
    Due to the nature of this option the following command line options will
1521
    be ignored when this one is used:
1522
      -u SUBJECT
1523
      -o message-header=EMAIL HEADER
1524
      -o message-charset=CHARSET
1525
      -a ATTACHMENT
1526
 
1527
 
1528
${colorGreen}The Message Body${colorNormal}
1529
The email message body may be specified in one of three ways:
1530
 1) Via the -m MESSAGE command line option.
1531
    Example:
1532
      -m "This is the message body"
1533
 
1534
 2) By putting the message body in a file and using the -o message-file=FILE
1535
    command line option.
1536
    Example:
1537
      -o message-file=/root/message.txt
1538
 
1539
 3) By piping the message body to sendEmail when nither of the above command
1540
    line options were specified.
1541
    Example:
1542
      grep "ERROR" /var/log/messages | sendEmail -t you\@domain.com ...
1543
 
1544
If the message body begins with "<html>" then the message will be treated as
1545
an HTML message and the MIME headers will be written so that a HTML capable
1546
email client will display the message in it's HTML form.
1547
Any of the above methods may be used with the -o message-format=raw option 
1548
to deliver an already complete email message.
1549
 
1550
 
1551
EOM
1552
            last CASE;
1553
        };
1554
 
1555
 
1556
 
1557
 
1558
 
1559
 
1560
## MISC
1561
        ($topic eq 'misc') && do {
1562
            print <<EOM;
1563
 
1564
${colorBold}MISC DOCUMENTATION${colorNormal}
1565
 
1566
${colorGreen}Misc Options${colorNormal}
1567
Options that don't fit anywhere else:
1568
    -a   ATTACHMENT [ATTACHMENT ...]
1569
    -xu  USERNAME
1570
    -xp  PASSWORD
1571
    -o   username=USERNAME
1572
    -o   password=PASSWORD
1573
    -o   tls=<auto|yes|no>
1574
    -o   timeout=SECONDS
1575
    -o   fqdn=FQDN
1576
 
1577
-a   ATTACHMENT [ATTACHMENT ...]
1578
    This option allows you to attach any number of files to your email message.
1579
    To specify more than one attachment, simply separate each filename with a
1580
    space.  Example: -a file1.txt file2.txt file3.txt
1581
 
1582
-xu  USERNAME
1583
    Alias for -o username=USERNAME
1584
 
1585
-xp  PASSWORD
1586
    Alias for -o password=PASSWORD
1587
 
1588
-o   username=USERNAME (synonym for -xu)
1589
    These options allow specification of a username to be used with SMTP
1590
    servers that require authentication.  If a username is specified but a
1591
    password is not, you will be prompted to enter one at runtime.
1592
 
1593
-o   password=PASSWORD (synonym for -xp)
1594
    These options allow specification of a password to be used with SMTP
1595
    servers that require authentication.  If a username is specified but a
1596
    password is not, you will be prompted to enter one at runtime. 
1597
 
1598
-o   tls=<auto|yes|no>
1599
    This option allows you to specify if TLS (SSL for SMTP) should be enabled
1600
    or disabled.  The default, auto, will use TLS automatically if your perl
1601
    installation has the IO::Socket::SSL and Net::SSLeay modules available,
1602
    and if the remote SMTP server supports TLS.  To require TLS for message
1603
    delivery set this to yes.  To disable TLS support set this to no.  A debug
1604
    level of one or higher will reveal details about the status of TLS.
1605
 
1606
-o   timeout=SECONDS
1607
    This option sets the timeout value in seconds used for all network reads,
1608
    writes, and a few other things.
1609
 
1610
-o   fqdn=FQDN
1611
    This option sets the Fully Qualified Domain Name used during the initial
1612
    SMTP greeting.  Normally this is automatically detected, but in case you
1613
    need to manually set it for some reason or get a warning about detection
1614
    failing, you can use this to override the default.
1615
 
1616
 
1617
EOM
1618
            last CASE;
1619
        };
1620
 
1621
 
1622
 
1623
 
1624
 
1625
 
1626
## NETWORKING
1627
        ($topic eq 'networking') && do {
1628
            print <<EOM;
1629
 
1630
${colorBold}NETWORKING DOCUMENTATION${colorNormal}
1631
 
1632
${colorGreen}Networking Options${colorNormal}
1633
Options related to networking:
1634
    -s   SERVER[:PORT]
1635
    -b   BINDADDR[:PORT]
1636
    -o   tls=<auto|yes|no>
1637
    -o   timeout=SECONDS
1638
 
1639
-s SERVER[:PORT]
1640
    This option allows you to specify the SMTP server sendEmail should
1641
    connect to to deliver your email message to.  If this option is not
1642
    specified sendEmail will try to connect to localhost:25 to deliver
1643
    the message.  THIS IS MOST LIKELY NOT WHAT YOU WANT, AND WILL LIKELY
1644
    FAIL unless you have a email server (commonly known as an MTA) running
1645
    on your computer!
1646
    Typically you will need to specify your company or ISP's email server.
1647
    For example, if you use CableOne you will need to specify:
1648
       -s mail.cableone.net
1649
    If you have your own email server running on port 300 you would
1650
    probably use an option like this:
1651
       -s myserver.mydomain.com:300
1652
    If you're a GMail user try:
1653
       -s smtp.gmail.com:587 -xu me\@gmail.com -xp PASSWD
1654
 
1655
-b BINDADDR[:PORT]
1656
    This option allows you to specify the local IP address (and optional
1657
    tcp port number) for sendEmail to bind to when connecting to the remote
1658
    SMTP server.  This useful for people who need to send an email from a
1659
    specific network interface or source address and are running sendEmail on
1660
    a firewall or other host with several network interfaces.
1661
 
1662
-o   tls=<auto|yes|no>
1663
    This option allows you to specify if TLS (SSL for SMTP) should be enabled
1664
    or disabled.  The default, auto, will use TLS automatically if your perl
1665
    installation has the IO::Socket::SSL and Net::SSLeay modules available,
1666
    and if the remote SMTP server supports TLS.  To require TLS for message
1667
    delivery set this to yes.  To disable TLS support set this to no.  A debug
1668
    level of one or higher will reveal details about the status of TLS.
1669
 
1670
-o timeout=SECONDS
1671
    This option sets the timeout value in seconds used for all network reads,
1672
    writes, and a few other things.
1673
 
1674
 
1675
EOM
1676
            last CASE;
1677
        };
1678
 
1679
 
1680
 
1681
 
1682
 
1683
 
1684
## OUTPUT
1685
        ($topic eq 'output') && do {
1686
            print <<EOM;
1687
 
1688
${colorBold}OUTPUT DOCUMENTATION${colorNormal}
1689
 
1690
${colorGreen}Output Options${colorNormal}
1691
Options related to output:
1692
    -l LOGFILE
1693
    -v
1694
    -q
1695
 
1696
-l LOGFILE
1697
    This option allows you to specify a log file to append to.  Every message
1698
    that is displayed to STDOUT is also written to the log file.  This may be
1699
    used in conjunction with -q and -v.
1700
 
1701
-q
1702
    This option tells sendEmail to disable printing to STDOUT.  In other
1703
    words nothing will be printed to the console.  This does not affect the
1704
    behavior of the -l or -v options.
1705
 
1706
-v
1707
    This option allows you to increase the debug level of sendEmail.  You may
1708
    either use this option more than once, or specify more than one v at a
1709
    time to obtain a debug level higher than one.  Examples:
1710
        Specifies a debug level of 1:  -v
1711
        Specifies a debug level of 2:  -vv
1712
        Specifies a debug level of 2:  -v -v
1713
    A debug level of one is recommended when doing any sort of debugging.  
1714
    At that level you will see the entire SMTP transaction (except the
1715
    body of the email message), and hints will be displayed for most
1716
    warnings and errors.  The highest debug level is three.
1717
 
1718
 
1719
EOM
1720
            last CASE;
1721
        };
1722
 
1723
        ## Unknown option selected!
1724
        quit("ERROR => The help topic specified is not valid!", 1);
1725
    };
1726
 
1727
exit(1);
1728
}
1729
 
1730
 
1731
 
1732
 
1733
 
1734
 
1735
 
1736
 
1737
 
1738
 
1739
 
1740
 
1741
 
1742
 
1743
 
1744
 
1745
 
1746
 
1747
 
1748
 
1749
 
1750
 
1751
#############################
1752
##                          ##
1753
##      MAIN PROGRAM         ##
1754
##                          ##
1755
#############################
1756
 
1757
 
1758
## Initialize
1759
initialize();
1760
 
1761
## Process Command Line
1762
processCommandLine();
1763
$conf{'alarm'} = $opt{'timeout'};
1764
 
1765
## Abort program after $conf{'alarm'} seconds to avoid infinite hangs
1766
alarm($conf{'alarm'}) if ($^O !~ /win/i);  ## alarm() doesn't work in win32
1767
 
1768
 
1769
 
1770
 
1771
###################################################
1772
##  Read $message from STDIN if -m was not used  ##
1773
###################################################
1774
 
1775
if (!($message)) {
1776
    ## Read message body from a file specified with -o message-file=
1777
    if ($opt{'message-file'}) {
1778
        if (! -e $opt{'message-file'}) {
1779
            printmsg("ERROR => Message body file specified [$opt{'message-file'}] does not exist!", 0);
1780
            printmsg("HINT => 1) check spelling of your file; 2) fully qualify the path; 3) doubble quote it", 1);
1781
            quit("", 1);
1782
        }
1783
        if (! -r $opt{'message-file'}) {
1784
            printmsg("ERROR => Message body file specified can not be read due to restricted permissions!", 0);
1785
            printmsg("HINT => Check permissions on file specified to ensure it can be read", 1);
1786
            quit("", 1);
1787
        }
1788
        if (!open(MFILE, "< " . $opt{'message-file'})) {
1789
            printmsg("ERROR => Error opening message body file [$opt{'message-file'}]: $!", 0);
1790
            quit("", 1);
1791
        }
1792
        while (<MFILE>) {
1793
            $message .= $_;
1794
        }
1795
        close(MFILE);
1796
    }
1797
 
1798
    ## Read message body from STDIN
1799
    else {
1800
        alarm($conf{'alarm'}) if ($^O !~ /win/i);  ## alarm() doesn't work in win32
1801
        if ($conf{'stdout'}) {
1802
            print "Reading message body from STDIN because the '-m' option was not used.\n";
1803
            print "If you are manually typing in a message:\n";
1804
            print "  - First line must be received within $conf{'alarm'} seconds.\n" if ($^O !~ /win/i);
1805
            print "  - End manual input with a CTRL-D on its own line.\n\n" if ($^O !~ /win/i);
1806
            print "  - End manual input with a CTRL-Z on its own line.\n\n" if ($^O =~ /win/i);
1807
        }
1808
        while (<STDIN>) {                 ## Read STDIN into $message
1809
            $message .= $_;
1810
            alarm(0) if ($^O !~ /win/i);  ## Disable the alarm since at least one line was received
1811
        }
1812
        printmsg("Message input complete.", 0);
1813
    }
1814
}
1815
 
1816
## Replace bare LF's with CRLF's (\012 should always have \015 with it)
1817
$message =~ s/(\015)?(\012|$)/\015\012/g;
1818
 
1819
## Replace bare CR's with CRLF's (\015 should always have \012 with it)
1820
$message =~ s/(\015)(\012|$)?/\015\012/g;
1821
 
1822
## Check message for bare periods and encode them
1823
$message =~ s/(^|$CRLF)(\.{1})($CRLF|$)/$1.$2$3/g;
1824
 
1825
## Get the current date for the email header
1826
my ($sec,$min,$hour,$mday,$mon,$year,$day) = gmtime();
1827
$year += 1900; $mon = return_month($mon); $day = return_day($day);
1828
my $date = sprintf("%s, %s %s %d %.2d:%.2d:%.2d %s",$day, $mday, $mon, $year, $hour, $min, $sec, $conf{'timezone'});
1829
 
1830
 
1831
 
1832
 
1833
##################################
1834
##  Connect to the SMTP server  ##
1835
##################################
1836
printmsg("DEBUG => Connecting to $conf{'server'}:$conf{'port'}", 1);
1837
$SIG{'ALRM'} = sub { 
1838
    printmsg("ERROR => Timeout while connecting to $conf{'server'}:$conf{'port'}  There was no response after $conf{'alarm'} seconds.", 0); 
1839
    printmsg("HINT => Try specifying a different mail relay with the -s option.", 1);
1840
    quit("", 1);
1841
};
1842
alarm($conf{'alarm'}) if ($^O !~ /win/i);  ## alarm() doesn't work in win32;
1843
$SERVER = IO::Socket::INET->new( PeerAddr  => $conf{'server'},
1844
                                 PeerPort  => $conf{'port'},
1845
                                 LocalAddr => $conf{'bindaddr'},
1846
                                 Proto     => 'tcp',
1847
                                 Autoflush => 1,
1848
                                 timeout   => $conf{'alarm'},
1849
);
1850
alarm(0) if ($^O !~ /win/i);  ## alarm() doesn't work in win32;
1851
 
1852
## Make sure we got connected
1853
if ( (!$SERVER) or (!$SERVER->opened()) ) {
1854
    printmsg("ERROR => Connection attempt to $conf{'server'}:$conf{'port'} failed: $@", 0);
1855
    printmsg("HINT => Try specifying a different mail relay with the -s option.", 1);
1856
    quit("", 1);
1857
}
1858
 
1859
## Save our IP address for later
1860
$conf{'ip'} = $SERVER->sockhost();
1861
printmsg("DEBUG => My IP address is: $conf{'ip'}", 1);
1862
 
1863
 
1864
 
1865
 
1866
 
1867
 
1868
 
1869
#########################
1870
##  Do the SMTP Dance  ##
1871
#########################
1872
 
1873
## Read initial greeting to make sure we're talking to a live SMTP server
1874
if (SMTPchat()) { quit($conf{'error'}, 1); }
1875
 
1876
## We're about to use $opt{'fqdn'}, make sure it isn't empty
1877
if (!$opt{'fqdn'}) {
1878
    ## Ok, that means we couldn't get a hostname, how about using the IP address for the HELO instead
1879
    $opt{'fqdn'} = "[" . $conf{'ip'} . "]";
1880
}
1881
 
1882
## EHLO
1883
if (SMTPchat('EHLO ' . $opt{'fqdn'}))   {
1884
    printmsg($conf{'error'}, 0);
1885
    printmsg("NOTICE => EHLO command failed, attempting HELO instead");
1886
    if (SMTPchat('HELO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); }
1887
    if ( $opt{'username'} and $opt{'password'} ) {
1888
        printmsg("WARNING => The mail server does not support SMTP authentication!", 0);
1889
    }
1890
}
1891
else {
1892
 
1893
    ## Determin if the server supports TLS
1894
    if ($conf{'SMTPchat_response'} =~ /STARTTLS/) {
1895
        $conf{'tls_server'} = 1;
1896
        printmsg("DEBUG => The remote SMTP server supports TLS :)", 2);
1897
    }
1898
    else {
1899
        $conf{'tls_server'} = 0;
1900
        printmsg("DEBUG => The remote SMTP server does NOT support TLS :(", 2);
1901
    }
1902
 
1903
    ## Start TLS if possible
1904
    if ($conf{'tls_server'} == 1 and $conf{'tls_client'} == 1 and $opt{'tls'} =~ /^(yes|auto)$/) {
1905
        printmsg("DEBUG => Starting TLS", 2);
1906
        if (SMTPchat('STARTTLS')) { quit($conf{'error'}, 1); }
1907
        if (! IO::Socket::SSL->start_SSL($SERVER, SSL_version => 'SSLv3 TLSv1')) {
1908
            quit("ERROR => TLS setup failed: " . IO::Socket::SSL::errstr(), 1);
1909
        }
1910
        printmsg("DEBUG => TLS: Using cipher: ". $SERVER->get_cipher(), 3);
1911
        printmsg("DEBUG => TLS session initialized :)", 1);
1912
 
1913
        ## Restart our SMTP session
1914
        if (SMTPchat('EHLO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); }
1915
    }
1916
    elsif ($opt{'tls'} eq 'yes' and $conf{'tls_server'} == 0) {
1917
        quit("ERROR => TLS not possible! Remote SMTP server, $conf{'server'},  does not support it.", 1);
1918
    }
1919
 
1920
 
1921
    ## Do SMTP Auth if required
1922
    if ( $opt{'username'} and $opt{'password'} ) {
1923
        if ($conf{'SMTPchat_response'} !~ /AUTH\s/) {
1924
            printmsg("NOTICE => Authentication not supported by the remote SMTP server!", 0);
1925
        }
1926
        else {
1927
            my $auth_succeeded = 0;
1928
            my $mutual_method = 0;
1929
 
1930
            # ## SASL CRAM-MD5 authentication method
1931
            # if ($conf{'SMTPchat_response'} =~ /\bCRAM-MD5\b/i) {
1932
            #     printmsg("DEBUG => SMTP-AUTH: Using CRAM-MD5 authentication method", 1);
1933
            #     if (SMTPchat('AUTH CRAM-MD5')) { quit($conf{'error'}, 1); }
1934
            #     
1935
            #     ## FIXME!!
1936
            #     
1937
            #     printmsg("DEBUG => User authentication was successful", 1);
1938
            # }
1939
 
1940
            ## SASL LOGIN authentication method
1941
            if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bLOGIN\b/i) {
1942
                $mutual_method = 1;
1943
                printmsg("DEBUG => SMTP-AUTH: Using LOGIN authentication method", 1);
1944
                if (!SMTPchat('AUTH LOGIN')) {
1945
                    if (!SMTPchat(base64_encode($opt{'username'}))) {
1946
                        if (!SMTPchat(base64_encode($opt{'password'}))) {
1947
                            $auth_succeeded = 1;
1948
                            printmsg("DEBUG => User authentication was successful (Method: LOGIN)", 1);
1949
                        }
1950
                    }
1951
                }
1952
                if ($auth_succeeded == 0) {
1953
                    printmsg("DEBUG => SMTP-AUTH: LOGIN authenticaion failed.", 1);
1954
                }
1955
            }
1956
 
1957
            ## SASL PLAIN authentication method
1958
            if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bPLAIN\b/i) {
1959
                $mutual_method = 1;
1960
                printmsg("DEBUG => SMTP-AUTH: Using PLAIN authentication method", 1);
1961
                if (SMTPchat('AUTH PLAIN ' . base64_encode("$opt{'username'}\0$opt{'username'}\0$opt{'password'}"))) {
1962
                    printmsg("DEBUG => SMTP-AUTH: PLAIN authenticaion failed.", 1);
1963
                }
1964
                else {
1965
                    $auth_succeeded = 1;
1966
                    printmsg("DEBUG => User authentication was successful (Method: PLAIN)", 1);
1967
                }
1968
            }
1969
 
1970
            ## If none of the authentication methods supported by sendEmail were supported by the server, let the user know
1971
            if ($mutual_method == 0) {
1972
                printmsg("WARNING => SMTP-AUTH: No mutually supported authentication methods available", 0);
1973
            }
1974
 
1975
            ## If we didn't get authenticated, log an error message and exit
1976
            if ($auth_succeeded == 0) {
1977
                quit("ERROR => ERROR => SMTP-AUTH: Authentication to $conf{'server'}:$conf{'port'} failed.", 1);
1978
            }
1979
        }
1980
    }
1981
}
1982
 
1983
## MAIL FROM
1984
if (SMTPchat('MAIL FROM:<' .(returnAddressParts($from))[1]. '>')) { quit($conf{'error'}, 1); }
1985
 
1986
## RCPT TO
1987
my $oneRcptAccepted = 0;
1988
foreach my $rcpt (@to, @cc, @bcc) {
1989
    my ($name, $address) = returnAddressParts($rcpt);
1990
    if (SMTPchat('RCPT TO:<' . $address . '>')) {
1991
        printmsg("WARNING => The recipient <$address> was rejected by the mail server, error follows:", 0);
1992
        $conf{'error'} =~ s/^ERROR/WARNING/o;
1993
        printmsg($conf{'error'}, 0);
1994
    }
1995
    elsif ($oneRcptAccepted == 0) {
1996
        $oneRcptAccepted = 1;
1997
    }
1998
}
1999
## If no recipients were accepted we need to exit with an error.
2000
if ($oneRcptAccepted == 0) {
2001
    quit("ERROR => Exiting. No recipients were accepted for delivery by the mail server.", 1);
2002
}
2003
 
2004
## DATA
2005
if (SMTPchat('DATA')) { quit($conf{'error'}, 1); }
2006
 
2007
 
2008
###############################
2009
##  Build and send the body  ##
2010
###############################
2011
printmsg("INFO => Sending message body",1);
2012
 
2013
## If the message-format is raw just send the message as-is.
2014
if ($opt{'message-format'} =~ /^raw$/i) {
2015
    print $SERVER $message;
2016
}
2017
 
2018
## If the message-format isn't raw, then build and send the message,
2019
else {
2020
 
2021
    ## Message-ID: <MessageID>
2022
    if ($opt{'message-header'} !~ /^Message-ID:/iom) {
2023
        $header .= 'Message-ID: <' . $conf{'Message-ID'} . '@' . $conf{'hostname'} . '>' . $CRLF;
2024
    }
2025
 
2026
    ## From: "Name" <address@domain.com> (the pointless test below is just to keep scoping correct)
2027
    if ($from and $opt{'message-header'} !~ /^From:/iom) {
2028
        my ($name, $address) = returnAddressParts($from);
2029
        $header .= 'From: "' . $name . '" <' . $address . '>' . $CRLF;
2030
    }
2031
 
2032
    ## Reply-To: 
2033
    if ($opt{'reply-to'} and $opt{'message-header'} !~ /^Reply-To:/iom) {
2034
        my ($name, $address) = returnAddressParts($opt{'reply-to'});
2035
        $header .= 'Reply-To: "' . $name . '" <' . $address . '>' . $CRLF;
2036
    }
2037
 
2038
    ## To: "Name" <address@domain.com>
2039
    if ($opt{'message-header'} =~ /^To:/iom) {
2040
        ## The user put the To: header in via -o message-header - dont do anything
2041
    }
2042
    elsif (scalar(@to) > 0) {
2043
        $header .= "To:";
2044
        for (my $a = 0; $a < scalar(@to); $a++) {
2045
            my $msg = "";
2046
 
2047
            my ($name, $address) = returnAddressParts($to[$a]);
2048
            $msg = " \"$name\" <$address>";
2049
 
2050
            ## If we're not on the last address add a comma to the end of the line.
2051
            if (($a + 1) != scalar(@to)) {
2052
                $msg .= ",";
2053
            }
2054
 
2055
            $header .= $msg . $CRLF;
2056
        }
2057
    }
2058
    ## We always want a To: line so if the only recipients were bcc'd they don't see who it was sent to
2059
    else {
2060
        $header .= "To: \"Undisclosed Recipients\" <>$CRLF";
2061
    }
2062
 
2063
    if (scalar(@cc) > 0 and $opt{'message-header'} !~ /^Cc:/iom) {
2064
        $header .= "Cc:";
2065
        for (my $a = 0; $a < scalar(@cc); $a++) {
2066
            my $msg = "";
2067
 
2068
            my ($name, $address) = returnAddressParts($cc[$a]);
2069
            $msg = " \"$name\" <$address>";
2070
 
2071
            ## If we're not on the last address add a comma to the end of the line.
2072
            if (($a + 1) != scalar(@cc)) {
2073
                $msg .= ",";
2074
            }
2075
 
2076
            $header .= $msg . $CRLF;
2077
        }
2078
    }
2079
 
2080
    if ($opt{'message-header'} !~ /^Subject:/iom) {
2081
        $header .= 'Subject: ' . $subject . $CRLF;                   ## Subject
2082
    }
2083
    if ($opt{'message-header'} !~ /^Date:/iom) {
2084
        $header .= 'Date: ' . $date . $CRLF;                         ## Date
2085
    }
2086
    if ($opt{'message-header'} !~ /^X-Mailer:/iom) {
2087
        $header .= 'X-Mailer: sendEmail-'.$conf{'version'}.$CRLF;    ## X-Mailer
2088
    }
2089
    ## I wonder if I should put this in by default?
2090
    # if ($opt{'message-header'} !~ /^X-Originating-IP:/iom) {
2091
    #     $header .= 'X-Originating-IP: ['.$conf{'ip'}.']'.$CRLF;      ## X-Originating-IP
2092
    # }
2093
 
2094
    ## Encode all messages with MIME.
2095
    if ($opt{'message-header'} !~ /^MIME-Version:/iom) {
2096
        $header .=  "MIME-Version: 1.0$CRLF";
2097
    }
2098
    if ($opt{'message-header'} !~ /^Content-Type:/iom) {
2099
        my $content_type = 'multipart/mixed';
2100
        if (scalar(@attachments) == 0) { $content_type = 'multipart/related'; }
2101
        $header .= "Content-Type: $content_type; boundary=\"$conf{'delimiter'}\"$CRLF";
2102
    }
2103
 
2104
    ## Send additional message header line(s) if specified
2105
    if ($opt{'message-header'}) {
2106
        $header .= $opt{'message-header'};
2107
    }
2108
 
2109
    ## Send the message header to the server
2110
    print $SERVER $header . $CRLF;
2111
 
2112
    ## Start sending the message body to the server
2113
    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";
2114
    print $SERVER "$CRLF";
2115
 
2116
 
2117
    ## Send message body
2118
    print $SERVER "--$conf{'delimiter'}$CRLF";
2119
    ## Send a message content-type header:
2120
    ## If the message contains HTML...
2121
    if ($opt{'message-content-type'} eq 'html' or ($opt{'message-content-type'} eq 'auto' and $message =~ /^\s*(<HTML|<!DOCTYPE)/i) ) {
2122
        printmsg("Setting content-type: text/html", 1);
2123
        print $SERVER "Content-Type: text/html;$CRLF";
2124
    }
2125
    ## Otherwise assume it's plain text...
2126
    elsif ($opt{'message-content-type'} eq 'text' or $opt{'message-content-type'} eq 'auto') {
2127
        printmsg("Setting content-type: text/plain", 1);
2128
        print $SERVER "Content-Type: text/plain;$CRLF";
2129
    }
2130
    ## If they've specified their own content-type string...
2131
    else {
2132
        printmsg("Setting custom content-type: ".$opt{'message-content-type'}, 1);
2133
        print $SERVER "Content-Type: ".$opt{'message-content-type'}.";$CRLF";
2134
    }
2135
    print $SERVER "        charset=\"" . $opt{'message-charset'} . "\"$CRLF";
2136
    print $SERVER "Content-Transfer-Encoding: 7bit$CRLF";
2137
    print $SERVER $CRLF . $message;
2138
 
2139
 
2140
 
2141
    ## Send Attachemnts
2142
    if (scalar(@attachments) > 0) {
2143
        ## Disable the alarm so people on modems can send big attachments
2144
        alarm(0) if ($^O !~ /win/i);  ## alarm() doesn't work in win32
2145
 
2146
        ## Send the attachments
2147
        foreach my $filename (@attachments) {
2148
            ## This is check 2, we already checked this above, but just in case...
2149
            if ( ! -f $filename ) {
2150
                printmsg("ERROR => The file [$filename] doesn't exist!  Email will be sent, but without that attachment.", 0);
2151
            }
2152
            elsif ( ! -r $filename ) {
2153
                printmsg("ERROR => Couldn't open the file [$filename] for reading: $!   Email will be sent, but without that attachment.", 0);
2154
            }
2155
            else {
2156
                printmsg("DEBUG => Sending the attachment [$filename]", 1);
2157
                send_attachment($filename);
2158
            }
2159
        }
2160
    }
2161
 
2162
 
2163
    ## End the mime encoded message
2164
    print $SERVER "$CRLF--$conf{'delimiter'}--$CRLF";  
2165
}
2166
 
2167
 
2168
## Tell the server we are done sending the email
2169
print $SERVER "$CRLF.$CRLF";
2170
if (SMTPchat()) { quit($conf{'error'}, 1); }
2171
 
2172
 
2173
 
2174
####################
2175
#  We are done!!!  #
2176
####################
2177
 
2178
## Disconnect from the server (don't SMTPchat(), it breaks when using TLS)
2179
print $SERVER "QUIT$CRLF";
2180
close $SERVER;
2181
 
2182
 
2183
 
2184
 
2185
 
2186
 
2187
#######################################
2188
##  Generate exit message/log entry  ##
2189
#######################################
2190
 
2191
if ($conf{'debug'} or $conf{'logging'}) {
2192
    printmsg("Generating a detailed exit message", 3);
2193
 
2194
    ## Put the message together
2195
    my $output = "Email was sent successfully!  From: <" . (returnAddressParts($from))[1] . "> ";
2196
 
2197
    if (scalar(@to) > 0) {
2198
        $output .= "To: ";
2199
        for ($a = 0; $a < scalar(@to); $a++) {
2200
            $output .= "<" . (returnAddressParts($to[$a]))[1] . "> ";
2201
        }
2202
    }
2203
    if (scalar(@cc) > 0) {
2204
        $output .= "Cc: ";
2205
        for ($a = 0; $a < scalar(@cc); $a++) {
2206
            $output .= "<" . (returnAddressParts($cc[$a]))[1] . "> ";
2207
        }
2208
    }
2209
    if (scalar(@bcc) > 0) {
2210
        $output .= "Bcc: ";
2211
        for ($a = 0; $a < scalar(@bcc); $a++) {
2212
            $output .= "<" . (returnAddressParts($bcc[$a]))[1] . "> ";
2213
        }
2214
    }
2215
    $output .= "Subject: [$subject] " if ($subject);
2216
    if (scalar(@attachments_names) > 0) { 
2217
        $output .= "Attachment(s): ";
2218
        foreach(@attachments_names) {
2219
            $output .= "[$_] ";
2220
        }
2221
    }
2222
    $output .= "Server: [$conf{'server'}:$conf{'port'}]";
2223
 
2224
 
2225
######################
2226
#  Exit the program  #
2227
######################
2228
 
2229
    ## Print / Log the detailed message
2230
    quit($output, 0);
2231
}
2232
else {
2233
    ## Or the standard message
2234
    quit("Email was sent successfully!", 0);
2235
}
2236