/[nagios-plugins-perl]/trunk/plugins/check_mail_imap.pl
ViewVC logotype

Contents of /trunk/plugins/check_mail_imap.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 214 - (show annotations) (download)
Wed Apr 17 10:37:07 2019 UTC (19 months, 2 weeks ago) by xhumbert
File MIME type: text/plain
File size: 16833 byte(s)
Check alt_names for certs
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2011-2019 St├ęphane Urbanovski <stephane.urbanovski@ac-nancy-metz.fr>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty
12 # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # you should have received a copy of the GNU General Public License
16 # along with this program (or with Nagios); if not, write to the
17 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 # Boston, MA 02111-1307, USA
19 #
20
21
22 use strict;
23 use warnings;
24
25 use POSIX qw(setlocale strftime);
26 use Locale::gettext;
27
28 use File::Basename; # get basename()
29 use Nagios::Plugin;
30
31 # use Sys::Hostname;
32 use Mail::IMAPClient;
33 use IO::Socket::SSL;
34
35 use POSIX qw(strftime);
36 use Time::Zone;
37
38 use Data::Dumper;
39
40
41 my $VERSION = '1.0';
42 my $TIMEOUT = 10;
43 my $PROGNAME = basename($0);
44
45 my $INBOX = 'INBOX';
46
47
48 # From DateTime::Format::Mail :
49 my $loose_RE = qr{
50 \;\s*
51 (?i:
52 (?:Mon|Tue|Wed|Thu|Fri|Sat|Sun|[A-Z][a-z][a-z]) ,? # Day name + comma
53 )?
54 # (empirically optional)
55 \s*
56 (\d{1,2}) # day of month
57 [-\s]*
58 (?i: (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ) # month
59 [-\s]*
60 ((?:\d\d)?\d\d) # year
61 \s+
62 (\d?\d):(\d?\d) (?: :(\d?\d) )? # time
63 (?:
64 \s+ "? (
65 [+-] \d{4} # standard form
66 | [A-Z]+ # obsolete form (mostly ignored)
67 | GMT [+-] \d+ # empirical (converted)
68 | [A-Z]+\d+ # bizarre empirical (ignored)
69 | [a-zA-Z/]+ # linux style (ignored)
70 | [+-]{0,2} \d{3,5} # corrupted standard form
71 ) "? # time zone (optional)
72 )?
73 (?: \s+ \([^\)]+\) )? # (friendly tz name; empirical)
74 \s* \.? $
75 }x;
76
77
78 # i18n :
79 setlocale(LC_MESSAGES, '');
80 textdomain('nagios-plugins-perl');
81
82 # Don't use locale format for dates :
83 # setlocale(LC_TIME, 'C');
84
85 my $np = Nagios::Plugin->new(
86 version => $VERSION,
87 blurb => _gt('Nagios plugins to check imap server. This plugins also allow you to periodicaly check a mail previously sent by check_mail_pop.pl Nagios plugin.'),
88 usage => "Usage: %s -H <imap host> -u <user> -p <password> [-t <timeout>] [ -c|--critical=<threshold> ] [ -w|--warning=<threshold> ]",
89 timeout => $TIMEOUT+1,
90 extra => &showExtra(),
91 );
92
93 $np->add_arg (
94 spec => 'host|H=s',
95 help => _gt('imap/imaps server.'),
96 required => 1,
97 );
98 $np->add_arg (
99 spec => 'user|u=s',
100 help => _gt('Username'),
101 required => 1,
102 );
103 $np->add_arg (
104 spec => 'password|p=s',
105 help => _gt('User password'),
106 required => 1,
107 );
108 $np->add_arg (
109 spec => 'port=i',
110 help => _gt('Server port ( default to 143 for imap and 993 for imaps).'),
111 required => 0,
112 default => 0,
113 );
114 $np->add_arg (
115 spec => 'proto|P=s',
116 help => _gt('Protocol imap(default)/imaps/tls.'),
117 default => 'auto',
118 required => 0,
119 );
120 $np->add_arg (
121 spec => 'key|k=s',
122 help => _gt('Subject key to search (see check_mail_smtp.pl)'),
123 required => 1,
124 );
125
126 $np->add_arg (
127 spec => 'wx=f',
128 help => _gt('Warning threshold for the maximum number of messages that may be seen in mailbox'),
129 default => 50,
130 );
131
132 $np->add_arg (
133 spec => 'cx=f',
134 help => _gt('Critical threshold for the maximum number of messages that may be seen in mailbox'),
135 default => 100,
136 );
137
138 $np->add_arg (
139 spec => 'wt=f',
140 help => _gt('Warning threshold for travel time (in seconds)'),
141 default => 60,
142 label => 'FLOAT'
143 );
144 $np->add_arg (
145 spec => 'ct=f',
146 help => _gt('Critical threshold for travel time (in seconds)'),
147 default => 600,
148 label => 'FLOAT'
149 );
150
151 $np->add_arg (
152 spec => 'wa=f',
153 help => _gt('Warning threshold for last message age (in seconds)'),
154 default => 600,
155 label => 'FLOAT'
156 );
157 $np->add_arg (
158 spec => 'ca=f',
159 help => _gt('Critical threshold for last message age (in seconds)'),
160 default => 1800,
161 label => 'FLOAT'
162 );
163
164 $np->add_arg (
165 spec => 'no-cert-check',
166 help => _gt('Disable SSL certificat checking'),
167 default => 0,
168 );
169 $np->add_arg (
170 spec => 'delete-old-messages=i',
171 help => _gt('Delete all messages older than this (days).'),
172 default => 0,
173 );
174 $np->add_arg (
175 spec => 'dry',
176 help => _gt('Dry run. Do not delete anything.'),
177 default => 0,
178 );
179
180
181 $np->getopts;
182 my $verbose = $np->opts->verbose;
183
184 my $server = $np->opts->get('host');
185 my $user = $np->opts->get('user');
186 my $password = $np->opts->get('password');
187
188 my $port = $np->opts->get('port');
189 my $proto = $np->opts->get('proto');
190
191 my $key = $np->opts->get('key');
192
193 my $warnMaxMsg = $np->opts->get('wx');
194 my $critMaxMsg = $np->opts->get('cx');
195
196 my $warnAge = $np->opts->get('wa');
197 my $critAge = $np->opts->get('ca');
198
199 my $warnTravTime = $np->opts->get('wt');
200 my $critTravTime = $np->opts->get('ct');
201
202 my $noCertCheck = $np->opts->get('no-cert-check');
203 $noCertCheck = 1;
204
205 IO::Socket::SSL::set_defaults('SSL_verify_mode' => SSL_VERIFY_NONE);
206
207 my $imap = Mail::IMAPClient->new(
208 SSL_verify_mode => $noCertCheck ? 0 : 1,
209 Debug => $verbose > 1 ? 1 : 0
210 );
211
212 my $deleteOldMessages = $np->opts->get('delete-old-messages');
213 my $dryRun = $np->opts->get('dry');
214
215 $imap->Server($server);
216
217 if ( $proto eq 'imaps' ) {
218 $imap->Ssl(1);
219 $imap->Port(993);
220 }
221
222 # Used for date parsing :
223 my %months = do { my $i = 1;
224 map { $_, $i++ } qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
225 };
226
227
228 logD("Connecting to '$server' with protocol '$proto'...");
229 if ( !$imap->connect() ) {
230 $np->nagios_exit(CRITICAL, sprintf(_gt('Failed to connect to \'%s\' : %s'),$server,$imap->LastError) );
231 }
232
233 if ( $proto eq 'imaps' ) {
234 my $socket = $imap->Socket();
235 my $error = &checkSSL($socket);
236 if ( $error ne 'OK' ) {
237 $np->nagios_exit(WARNING, sprintf(_gt('SSL checks failed : %s'),$error) );
238 }
239 }
240
241
242 logD('Retrieving capabilities ...');
243 my $capabilities = $imap->capability;
244 if ( !$capabilities ) {
245 $np->nagios_exit(CRITICAL, sprintf(_gt('Failed to get IMAP capabilities : %s'),$imap->LastError) );
246 }
247 logD(' Capabilities: '.join(',',@{$capabilities}));
248
249 my $canTLS = 0;
250 foreach my $cap (@{$capabilities}) {
251 if ($cap eq 'STARTTLS') {
252 $canTLS = 1;
253 last;
254 }
255 }
256 if ( $proto eq 'tls' ) {
257 if ( $canTLS == 0 ) {
258 $np->nagios_exit(CRITICAL, _gt('Server has no STARTTLS capabilitiy') );
259 }
260 if ( !$imap->starttls() ) {
261 $np->nagios_exit(CRITICAL, sprintf(_gt('STARTTLS failed : %s'),imap->LastError) );
262 }
263 }
264
265
266 logD("Login with user '$user' ...");
267 $imap->User($user);
268 $imap->Password($password);
269 if ( !$imap->login() ) {
270 $np->nagios_exit(CRITICAL, sprintf(_gt("Enable to login with user '%s' : %s"),$user,$imap->LastError) );
271 }
272
273
274 my $msgcount = $imap->message_count($INBOX);
275 logD(" Found $msgcount messages in '$INBOX'");
276 if ( !$msgcount ) {
277 $np->nagios_exit(CRITICAL, sprintf(_gt("Failed to get message count in folder '%s' for user '%s' : %s"),$INBOX,$user,$imap->LastError) );
278 }
279 # Msg count check is deferred after reading MBOX
280
281 if ( !$imap->select( $INBOX ) ) {
282 $np->nagios_exit(CRITICAL, sprintf(_gt("Failed to select '%s' folder for user '%s' : %s"),$INBOX,$user,$imap->LastError) );
283 }
284
285
286
287 if ( $deleteOldMessages > 0 ) {
288
289 my $oldTs = time() - $deleteOldMessages * 24 * 3600;
290 my $Rfc2060_date = $imap->Rfc2060_date($oldTs);
291
292
293 my @oldMessages = $imap->sentbefore($Rfc2060_date) ;
294
295 logD(sprintf("Deleting %d messages older than %s", scalar(@oldMessages), $Rfc2060_date));
296
297 my @buffer = ();
298 my $run = 1;
299 while ($run) {
300 if ( my $msg = shift(@oldMessages) ) {
301 push(@buffer, $msg);
302 # logD("ID : $msg");
303 } else {
304 $run = 0;
305 }
306
307 if ( @buffer > 500 || ( $run == 0 && @buffer > 0) ) {
308 my $header = $imap->parse_headers(\@buffer,'Date','Subject');
309 foreach my $msgId ( keys %{$header}) {
310 logD(sprintf ("MSG %7i - %s : '%s'",$msgId,$header->{$msgId}{'Date'}[0],$header->{$msgId}{'Subject'}[0]) );
311 }
312 deleteMessages(\@buffer);
313 @buffer = ();
314
315 }
316 }
317
318 }
319
320
321 logD("Searching messages with '$key' key ...");
322 # my $search = $imap->search('TEXT "'.$key.'"');
323
324 my $search;
325
326 if ( $key eq 'ALL' ) {
327 $search = $imap->search('ALL');
328 $dryRun = 1; # Stay safe ...
329 } else {
330 $search = $imap->search('SUBJECT',$imap->Quote($key));
331 }
332
333
334 # print Dumper($search);
335
336 if ( !defined($search) ) {
337 $np->nagios_exit(CRITICAL, sprintf(_gt("Search messages with key '%s' failed : %s"),$key, $imap->LastError) );
338 }
339 if ( scalar(@{$search}) == 0 ) {
340 $np->nagios_exit(CRITICAL, sprintf(_gt("No message with key '%s' found in folder '%s'"),$key,$INBOX) );
341 }
342
343 logD(" Found ".scalar(@{$search})." messages with '$key' key :");
344
345
346
347 my @toDelete = ();
348 my $lastTimeSent = { 'TS' => -1};
349 my ($lastMsgId,$lastHeader) = (-1,undef);
350
351 my $msgCount = scalar(@{$search});
352
353 my @buffer = ();
354 # print Dumper($search);
355 my $run = 1;
356 while ($run) {
357
358 if ( my $msg = shift(@{$search}) ) {
359 push(@buffer, $msg);
360 # logD("ID : $msg");
361 } else {
362 $run = 0;
363 }
364
365 if ( @buffer > 50 || $run == 0 ) {
366 my $header = $imap->parse_headers(\@buffer,'Date','Received','Subject');
367 foreach my $msgId ( keys %{$header}) {
368
369 logD(sprintf (" - MSG %7i : '%s'",$msgId,$header->{$msgId}{'Subject'}[0]) );
370 if ($header->{$msgId}{'Subject'}[0] !~ /\b$key$/) {
371 # not an exact match ...
372 logW(' Subject does not match !');
373 next;
374 }
375
376 if ( !defined($header->{$msgId}{'Date'}[0]) ) {
377 logW(' Sent date not defined !');
378 next;
379 }
380 logD(' HEADER Date: '.$header->{$msgId}{'Date'}[0]);
381
382 my $timeSent = parseRFC2882Date(';'.$header->{$msgId}{'Date'}[0]);
383
384 if ( !defined($timeSent) ) {
385 logW(' Can\'t parse sent date: '.$header->{$msgId}{'Date'}[0]);
386 next;
387 }
388
389 # logD(' Sent date: '.strftime('%F %T',$timeSent->{'S'},$timeSent->{'M'},$timeSent->{'H'},$timeSent->{'d'},$timeSent->{'m'}-1,$timeSent->{'Y'}-1900));
390 # logD(' Sent date: '.gmtime($timeSent->{'TS'}));
391
392
393 if ( $timeSent->{'TS'} > $lastTimeSent->{'TS'} ) {
394 if ( $lastMsgId != -1 ) {
395 push(@toDelete, $lastMsgId);
396 }
397
398 $lastTimeSent = $timeSent;
399 $lastMsgId = $msgId;
400 $lastHeader = $header->{$msgId};
401 }
402
403 }
404
405 # Delete all messages except the last one
406 foreach my $msgId ( @buffer) {
407
408 next if ( $msgId eq $lastMsgId );
409
410 push(@toDelete, $msgId);
411
412 }
413
414 deleteMessages(\@toDelete);
415 @toDelete = ();
416 @buffer = ();
417
418 }
419 }
420
421
422 if ( !defined($lastHeader) ) {
423 $np->nagios_exit(CRITICAL, sprintf(_gt("No message using key '%s' found !"),$key));
424 }
425
426 my $tt = -1; # travel time (from sent to last received)
427
428
429
430 logD('Last messageId: '.$lastMsgId);
431 logD(' Now (GMT) : '.localtime());
432 logD(' HEADER Date : '.( $lastHeader->{'Date'}[0] || '?'));
433 logD(' Sent date (GMT) : '.localtime($lastTimeSent->{'TS'}));
434
435
436
437 logD(' HEADER Received : '.( $lastHeader->{'Received'}[0] || '?'));
438
439
440 my $timeReceived = parseRFC2882Date($lastHeader->{'Received'}[0]);
441 if ( $timeReceived ) {
442
443 # logD('Received : '.strftime('%F %T',$timeReceived->{'S'},$timeReceived->{'M'},$timeReceived->{'H'},$timeReceived->{'d'},$timeReceived->{'m'}-1,$timeReceived->{'Y'}-1900).' / '.$timeReceived->{'tz'});
444 logD(' Received date (GMT): '.localtime($timeReceived->{'TS'}));
445 # if ( $lastTimeSent->{'tz'} eq $timeReceived->{'tz'}) {
446 $tt = $timeReceived->{'TS'} - $lastTimeSent->{'TS'};
447 logD("Travel time : ${tt}s");
448
449 if ( $tt < 0 ) {
450 $np->add_message(WARNING, sprintf(_gt("Found negative travel time ! (%s)"),printAge($tt)) );
451
452 } else {
453 my $statusTravTim = $np->check_threshold(
454 'check' => $tt,
455 'warning' => $warnTravTime,
456 'critical' => $critTravTime,
457 );
458
459 $np->add_perfdata(
460 'label' => 'TravelTime',
461 'value' => $tt,
462 'min' => 0,
463 'uom' => 's',
464 'threshold' => $np->threshold()
465 );
466
467 if ( $statusTravTim == WARNING) {
468 $np->add_message($statusTravTim, sprintf(_gt("Travel time '%s' too long ! (%s)"),$key,printAge($tt)) );
469 } elsif ( $statusTravTim == CRITICAL) {
470 $np->add_message($statusTravTim, sprintf(_gt("Travel time '%s' too long !! (%s)"),$key,printAge($tt)) );
471 }
472 }
473
474 # } else {
475 # logW('Different timezone : '.$lastTimeSent->{'tz'}.' / '.$timeReceived->{'tz'});
476 # }
477 }
478
479 #my $now =
480
481 my $age = time() - $lastTimeSent->{'TS'}; # delta time (from sent to now)
482 logD("Age : ${age}s");
483
484 my $statusAge = $np->check_threshold(
485 'check' => $age,
486 'warning' => $warnAge,
487 'critical' => $warnAge,
488 );
489
490 $np->add_perfdata(
491 'label' => 'Age',
492 'value' => $age,
493 'min' => 0,
494 'uom' => 's',
495 'threshold' => $np->threshold()
496 );
497
498 if ( $statusAge == WARNING) {
499 $np->add_message($statusAge, sprintf(_gt("Last message '%s' too old ! (%s)"),$key,printAge($age)) );
500 } elsif ( $statusAge == CRITICAL) {
501 $np->add_message($statusAge, sprintf(_gt("Last message '%s' too old !! (%s)"),$key,printAge($age)) );
502 }
503
504
505 $imap->logout;
506 ## DEBUG
507 # $msgcount = 101;
508
509 my $statusMsgCount = $np->check_threshold(
510 'check' => $msgcount,
511 'warning' => $warnMaxMsg,
512 'critical' => $warnMaxMsg,
513 );
514
515 $np->add_perfdata(
516 'label' => 'MailboxCount',
517 'value' => $msgcount,
518 'min' => 0,
519 'uom' => '',
520 'threshold' => $np->threshold()
521 );
522
523
524 if ( $msgcount > $warnMaxMsg ) {
525 $np->add_message($statusMsgCount, sprintf(_gt('Mail system works fine (key \'%s\', age%s, travel%s, msg %d), but too many messages in INBOX: %d'),$key,printAge($age),printAge($tt),$msgcount,$msgcount) );
526 } else {
527 $np->add_message($statusMsgCount, sprintf(_gt('Mail system works fine (key \'%s\', age%s, travel%s, msg %d)'),$key,printAge($age),printAge($tt), $msgcount) );
528 }
529 my ($status, $message) = $np->check_messages('join' => ' ');
530 $np->nagios_exit($status, $message );
531
532
533 sub deleteMessages {
534
535 my ($toDelete) = @_;
536 if ( scalar(@{$toDelete}) ) {
537 logD('Deleting messages :'.join(',',@{$toDelete}));
538 if ( $dryRun ) {
539 logD('DRY-RUN : Will not delete message!');
540 return;
541 }
542 my $deleted = $imap->delete_message($toDelete);
543 logD('Deleted :'.$deleted);
544 if ( !$deleted ) {
545 $np->add_message(WARNING, sprintf(_gt('Delete old messages failed: %s'),$@) );
546 }
547 if ( !$imap->expunge($INBOX) ) {
548 $np->add_message(WARNING, sprintf(_gt('Expunge deleted messages on \'%s\' failed: %s'),$INBOX,$@) );
549 }
550 }
551 }
552
553 sub parseRFC2882Date {
554 # Sun, 20 Mar 2011 13:10:35 +0100 (CET)
555 my ($date) = @_;
556 my @parsed = $date =~ $loose_RE;
557 if ( @parsed ) {
558 my %when;
559 @when{qw( d m Y H M S tz)} = @parsed;
560 $when{'m'} = $months{"\L\u".$when{'m'}};
561 $when{'S'} ||= 0;
562 map {$_+0} ($when{'Y'},$when{'m'},$when{'d'});
563 # $when{'TS'} = strftime('%s',$when{'S'},$when{'M'},$when{'H'},$when{'d'},$when{'m'}-1,$when{'Y'}-1900) ;
564 $when{'TS'} = POSIX::mktime($when{'S'},$when{'M'},$when{'H'},$when{'d'},$when{'m'}-1,$when{'Y'}-1900) ;
565 if ( $when{'tz'} =~ /^[\+\-](\d\d)00/) {
566 $when{'TS'} += - tz_offset($when{'tz'}) + tz_local_offset();
567 }
568
569 return \%when;
570 }
571 return undef;
572 }
573
574
575 sub checkSSL {
576 return 'OK' if ( $noCertCheck );
577 my ($socket) = @_;
578 if ( ref($socket) ne 'IO::Socket::SSL' ) {
579 return _gt('No an IO::Socket::SSL socket ! ');
580 }
581
582 my $serverNameOk = 0;
583
584 my $cn = $socket->peer_certificate('commonName');
585 $cn =~ s/.*CN=(.*)$/$1/;
586 logD('SSL CN='.$cn);
587
588 if ($cn eq $server) {
589 $serverNameOk++;
590 } else {
591 my @altNames = $socket->peer_certificate('subjectAltNames');
592 my $i = 0;
593 do {
594 if ( !defined($altNames[$i])) {
595 last;
596 }
597 my $altNameType = $altNames[$i++];
598 my $altName = $altNames[$i++];
599
600 if ( $server eq $altName ) {
601 $serverNameOk++;
602 }
603 logD(sprintf('SSL subjectAltNames=%s (type=%s) : %d',$altName,$altNameType,$serverNameOk));
604
605 } while ($serverNameOk == 0);
606
607 }
608 my $issuer = $socket->peer_certificate('authority');
609 logD('SSL issuer='.$issuer);
610
611 if ( !$serverNameOk ) {
612 return _gt(sprintf('Server name does not match CN (%s) or subjectAltNames !',$cn));
613 }
614
615 return 'OK';
616 }
617
618 sub printAge {
619 my ($sec) = @_;
620 my ($s,$d,$h,$m) = ($sec,0,0,0);
621 my $ret = '';
622 if ( $s / (3600*24) > 1) {
623 $d = int($s / (3600*24));
624 $s = $s % (3600*24);
625 $ret .= " $d "._gt("days");
626 }
627 if ( $s / (3600) > 1) {
628 $h = int($s / 3600);
629 $s = $s % 3600;
630 $ret .= " $h "._gt("h");
631 }
632 if ( $s / (60) > 1) {
633 $m = int($s / 60);
634 $s = $s % 60;
635 $ret .= " $m "._gt("mn");
636 }
637 $ret .= " $s "._gt("s");
638 return $ret;
639 }
640
641
642
643 sub logD {
644 print STDERR 'DEBUG: '.$_[0]."\n" if ($verbose);
645 }
646 sub logW {
647 print STDERR 'WARNING: '.$_[0]."\n" if ($verbose);
648 }
649 # Gettext wrapper
650 sub _gt {
651 return gettext($_[0]);
652 }
653
654 sub showExtra {
655 return <<EOT;
656 (c)2011-2017 St├ęphane Urbanovski <s.urbanovski\@ac-nancy-metz.fr>
657
658 Note:
659
660 This plugin is design to run whith check_mail_smtp plugin.
661 It send a mail with a special "tag" in the subject. This tag is checked by
662 check_mail_(pop|imap) plugin to test your messaging system.
663
664 Use a dedicated mailbox for this check.
665
666 Example:
667 check_mail_imap.pl -H imap.example.org -u testbox1 -p pass -k 'key-testbox1-from-outside' --proto imaps
668
669 EOT
670 }

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.8