Subversion Repositories sysadmin_scripts

Rev

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

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