-
Notifications
You must be signed in to change notification settings - Fork 460
/
check_mail
executable file
·550 lines (468 loc) · 19.4 KB
/
check_mail
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# tails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
import os
import re
import sys
import ast
import time
import email
import base64
import getopt
import poplib
import socket
import imaplib
import cmk.utils.password_store
cmk.utils.password_store.replace_passwords()
def parse_exception(exc):
exc = str(exc)
if exc[0] == '{':
exc = "%d - %s" % list(ast.literal_eval(exc).values())[0]
return str(exc)
def output_check_result(rc, s, perfdata):
stxt = ['OK', 'WARN', 'CRIT', 'UNKNOWN'][rc]
output = '%s - %s' % (stxt, s)
if perfdata:
perfdata_output_entries = ['%s=%s' % (p[0], ';'.join(map(str, p[1:]))) for p in perfdata]
output += ' | %s' % " ".join(perfdata_output_entries)
sys.stdout.write('%s\n' % output)
def usage(msg=None):
if msg:
sys.stderr.write('ERROR: %s\n' % msg)
sys.stderr.write("""
USAGE: check_mail [OPTIONS]
OPTIONS:
--protocol PROTO Set to "IMAP" or "POP3", depending on your mailserver
(defaults to IMAP)
--server ADDRESS Host address of the IMAP/POP3 server hosting your mailbox
--port PORT IMAP or POP3 port
(defaults to 110 for POP3 and 995 for POP3 with SSL and
143 for IMAP and 993 for IMAP with SSL)
--username USER Username to use for IMAP/POP3
--password PW Password to use for IMAP/POP3
--ssl Use SSL for feching the mailbox (disabled by default)
--connect-timeout Timeout in seconds for network connects (defaults to 10)
--forward-ec Forward matched mails to the event console (EC)
--forward-method M Configure how to connect to the event console to forward
the messages to. Can be configured to:
udp,<ADDR>,<PORT> - Connect to remove EC via UDP
tcp,<ADDR>,<PORT> - Connect to remove EC via TCP
spool: - Write to site local spool directory
spool:/path/to/spooldir - Spool to given directory
/path/to/pipe - Write to given EC event pipe
Defaults to use the event console of the local OMD sites.
--forward-facility F Syslog facility to use for forwarding (Defaults to "2" -> mail)
--forward-app APP Specify which string to use for the syslog application field
when forwarding to the event console. You can specify macros like
\1 or \2 when you specified "--match-subject" with regex groups.
(Defaults to use the whole subject of the e mail)
--forward-host HOST Hostname to use for the generated events
--body-limit NUM Limit the number of characters of the body to forward
(Defaults to 1000)
--match-subject REGEX Use this option to not process all messages found in the inbox,
but only the whones whose subject matches the given regular expression.
--cleanup METHOD Delete processed messages (see --match-subject) or move to subfolder a
matching the given path. This is configured with the following METHOD:
delete - Simply delete mails
path/to/subfolder - Move to this folder (Only supported with IMAP)
By default the mails are not cleaned up, which might make your mailbox
grow when you not clean it up manually.
-d, --debug Enable debug mode
-h, --help Show this help message and exit
""")
sys.exit(1)
class ConnectError(Exception):
pass
class FetchMailsError(Exception):
pass
class CleanupMailboxError(Exception):
pass
class ForwardToECError(Exception):
pass
def connect(fetch_server, fetch_port, fetch_user, fetch_pass, fetch_ssl):
global g_M
global opt_debug
global fetch_proto
try:
if fetch_proto == 'POP3':
fetch_class = poplib.POP3_SSL if fetch_ssl else poplib.POP3
g_M = fetch_class(fetch_server, fetch_port)
g_M.user(fetch_user)
g_M.pass_(fetch_pass)
else:
fetch_class = imaplib.IMAP4_SSL if fetch_ssl else imaplib.IMAP4
g_M = fetch_class(fetch_server, fetch_port)
g_M.login(fetch_user, fetch_pass)
g_M.select('INBOX', readonly=False) # select INBOX
except Exception as e:
if opt_debug:
raise
raise ConnectError('Failed connect to %s:%d: %s' %
(fetch_server, fetch_port, parse_exception(e)))
def fetch_mails(match_subject):
global g_M
global opt_debug
global fetch_proto
mails = {}
try:
# Get mails from mailbox
if fetch_proto == 'POP3':
num_messages = len(g_M.list()[1])
for i in range(num_messages):
index = i + 1
lines = g_M.retr(index)[1]
mails[i] = email.message_from_string("\n".join(lines))
else:
retcode, messages = g_M.search(None, 'NOT', 'DELETED')
if retcode == 'OK' and messages[0].strip():
for num in messages[0].split(' '):
try:
data = g_M.fetch(num, '(RFC822)')[1]
mails[num] = email.message_from_string(data[0][1])
except Exception as e:
raise Exception('Failed to fetch mail %s (%s). Available messages: %r' %
(num, parse_exception(e), messages))
if not match_subject:
return mails
# Now filter out the messages not wanted to be handled by this check
return {
index: msg
for index, msg in mails.items()
if match_subject.match(msg.get('Subject', ''))
}
except Exception as e:
if opt_debug:
raise
raise FetchMailsError('Failed to check for mails: %s' % parse_exception(e))
def cleanup_mailbox(forwarded, cleanup_messages):
global g_M
global opt_debug
global fetch_proto
if not g_M:
return # do not deal with mailbox when none sent yet
try:
# Do not delete all messages in the inbox. Only the ones which were
# processed before! In the meantime there might be occured new ones.
for index in forwarded:
if fetch_proto == 'POP3':
if cleanup_messages == 'delete':
response = g_M.dele(index + 1)
if not response.startswith("+OK"):
raise Exception("Response from server: [%s]" % response)
else:
if cleanup_messages != 'delete':
# The user wants the message to be moved to the folder
# refered by the string stored in "cleanup_messages"
folder = cleanup_messages.strip('/')
# Create maybe missing folder hierarchy
target = ''
for level in folder.split('/'):
target += "%s/" % level
g_M.create(target)
# Copy the mail
ty, data = g_M.copy(str(index), folder)
if ty != 'OK':
raise Exception("Response from server: [%s]" % data)
# Now delete the mail
ty, data = g_M.store(index, '+FLAGS', '\\Deleted')
if ty != 'OK':
raise Exception("Response from server: [%s]" % data)
if fetch_proto == 'IMAP':
g_M.expunge()
except Exception as e:
if opt_debug:
raise
raise CleanupMailboxError('Failed to delete mail: %s' % parse_exception(e))
def close_mailbox():
global g_M
global fetch_proto
if not g_M:
return # do not deal with mailbox when none sent yet
if fetch_proto == 'POP3':
g_M.quit()
else:
g_M.close()
g_M.logout()
def syslog_time():
localtime = time.localtime()
day = int(time.strftime("%d", localtime)) # strip leading 0
value = time.strftime("%b %%d %H:%M:%S", localtime)
return value % day
def prepare_messages_for_ec(mails, match_subject, forward_app, forward_facility, forward_host,
fetch_server, body_limit):
# create syslog message from each mail
# <128> Oct 24 10:44:27 Klappspaten /var/log/syslog: Oct 24 10:44:27 Klappspaten logger: asdasdad as
# <facility+priority> timestamp hostname application: message
messages = []
cur_time = syslog_time()
priority = 5 # OK
forwarded = []
for index, msg in sorted(mails.items()):
subject = msg.get('Subject', 'None')
encoding = msg.get('Content-Transfer-Encoding', 'None')
log_line = subject
# Now add the body to the event
if msg.is_multipart():
# only care for the first text/plain element
for part in msg.walk():
content_type = part.get_content_type()
disposition = str(part.get('Content-Disposition'))
encoding = part.get('Content-Transfer-Encoding', 'None')
if content_type == 'text/plain' and 'attachment' not in disposition:
payload = part.get_payload()
if encoding == "base64":
payload = base64.b64decode(payload)
log_line += '|' + payload[:body_limit]
break
else:
payload = msg.get_payload()
if encoding == "base64":
payload = base64.b64decode(payload)
log_line += '|' + payload[:body_limit]
log_line = log_line.replace('\r\n', '\0')
log_line = log_line.replace('\n', '\0')
# replace match groups in "forward_app"
if forward_app:
application = forward_app
matches = match_subject.match(subject)
for num, match in enumerate(matches.groups()):
application = application.replace('\\%d' % (num + 1,), match)
else:
application = subject.replace('\n', '')
# Construct the final syslog message
log = '<%d>%s' % (forward_facility + priority, cur_time)
log += ' %s %s: %s' % (forward_host or fetch_server, application, log_line)
messages.append(log)
forwarded.append(index)
return messages, forwarded
def forward_to_ec(conn_timeout, messages, forwarded, cleanup_messages, forward_method,
forward_host):
# send lines to event console
# a) local in same omd site
# b) local pipe
# c) remote via udp
# d) remote via tcp
if not forward_method:
forward_method = os.getenv('OMD_ROOT') + "/tmp/run/mkeventd/eventsocket"
elif forward_method == 'spool:':
forward_method += os.getenv('OMD_ROOT') + "/var/mkeventd/spool"
socket.setdefaulttimeout(conn_timeout)
try:
if messages:
if isinstance(forward_method, tuple):
# connect either via tcp or udp
if forward_method[0] == 'udp':
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((forward_method[1], forward_method[2]))
for message in messages:
sock.send(message + "\n")
sock.close()
elif not forward_method.startswith('spool:'): # pylint: disable=no-member
# write into local event pipe
# Important: When the event daemon is stopped, then the pipe
# is *not* existing! This prevents us from hanging in such
# situations. So we must make sure that we do not create a file
# instead of the pipe!
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(forward_method)
sock.send('\n'.join(messages) + '\n')
sock.close()
else:
# Spool the log messages to given spool directory.
# First write a file which is not read into ec, then
# perform the move to make the file visible for ec
spool_path = forward_method[6:]
file_name = '.%s_%d_%d' % (forward_host, os.getpid(), time.time())
if not os.path.exists(spool_path):
os.makedirs(spool_path)
open('%s/%s' % (spool_path, file_name), 'w').write('\n'.join(messages) + '\n')
os.rename('%s/%s' % (spool_path, file_name), '%s/%s' % (spool_path, file_name[1:]))
if cleanup_messages:
cleanup_mailbox(forwarded, cleanup_messages)
return 0, 'Forwarded %d messages to event console' % len(messages), [('messages',
len(messages))]
except Exception as e:
raise ForwardToECError(
'Unable to forward messages to event console (%s). Left %d messages untouched.' %
(e, len(messages)))
g_M = None
opt_debug = False
fetch_proto = 'IMAP'
def parse_arguments(sys_args):
global g_M
global opt_debug
global fetch_proto
if sys_args is None:
sys_args = sys.argv[1:]
short_options = 'dh'
long_options = [
'protocol=',
'server=',
'port=',
'username=',
'password=',
'ssl',
'connect-timeout=',
'cleanup=',
'forward-ec',
'match-subject=',
'forward-facility=',
'forward-app=',
'forward-method=',
'forward-host=',
'body-limit=',
'help',
'debug',
]
required_params = [
'server',
'username',
'password',
]
try:
opts, _args = getopt.getopt(sys_args, short_options, long_options)
except getopt.GetoptError as err:
sys.stderr.write("%s\n" % err)
sys.exit(1)
fetch_server = None
fetch_port = None
fetch_user = None
fetch_pass = None
fetch_ssl = False
conn_timeout = 10
cleanup_messages = ""
forward_ec = False
forward_facility = 16 # default to "mail" (2 << 3)
forward_app = None
forward_method = None # local event console
forward_host = ''
match_subject = None
body_limit = 1000
for o, a in opts:
if o in ['-h', '--help']:
usage()
elif o in ['-d', '--debug']:
opt_debug = True
elif o == '--protocol':
fetch_proto = a
elif o == '--server':
fetch_server = a
elif o == '--port':
fetch_port = int(a)
elif o == '--username':
fetch_user = a
elif o == '--password':
fetch_pass = a
elif o == '--ssl':
fetch_ssl = True
elif o == '--connect-timeout':
conn_timeout = int(a)
elif o == '--cleanup':
cleanup_messages = a
elif o == '--forward-ec':
forward_ec = True
elif o == '--match-subject':
match_subject = re.compile(a)
elif o == '--forward-facility':
forward_facility = int(a) << 3
elif o == '--forward-app':
forward_app = a
elif o == '--forward-method':
if ',' in a:
forward_method = tuple(a.split(','))
else:
forward_method = a
elif o == '--forward-host':
forward_host = a
elif o == '--body-limit':
body_limit = int(a)
d_opts = dict(opts)
for required_param in required_params:
required_opt = "--%s" % required_param
if required_opt not in d_opts:
usage('The needed parameter %s is missing' % required_opt)
if fetch_proto not in ['IMAP', 'POP3']:
usage('The given protocol is not supported.')
if fetch_port is None:
if fetch_proto == 'POP3':
fetch_port = 995 if fetch_ssl else 110
else:
fetch_port = 993 if fetch_ssl else 143
return (
fetch_server,
fetch_port,
fetch_user,
fetch_pass,
fetch_ssl,
conn_timeout,
cleanup_messages,
forward_ec,
forward_facility,
forward_app,
forward_method,
forward_host,
match_subject,
body_limit,
)
def main(sys_args=None):
global g_M
global opt_debug
global fetch_proto
fetch_server, fetch_port, fetch_user, fetch_pass, fetch_ssl, conn_timeout,\
cleanup_messages, forward_ec, forward_facility, forward_app, forward_method,\
forward_host, match_subject, body_limit = parse_arguments(sys_args)
# Enable showing protocol messages of imap for debugging
if fetch_proto == 'IMAP' and opt_debug:
imaplib.Debug = 4
try:
connect(fetch_server, fetch_port, fetch_user, fetch_pass, fetch_ssl)
if forward_ec:
mails = fetch_mails(match_subject)
messages, forwarded = prepare_messages_for_ec(mails, match_subject, forward_app,
forward_facility, forward_host,
fetch_server, body_limit)
return forward_to_ec(conn_timeout, messages, forwarded, cleanup_messages,
forward_method, forward_host)
return 0, 'Successfully logged in to mailbox', None
except ConnectError as e:
return 3, str(e), None
except FetchMailsError as e:
return 3, str(e), None
except CleanupMailboxError as e:
return 2, str(e), None
except ForwardToECError as e:
return 3, str(e), None
except Exception as e:
if opt_debug:
raise
return 2, 'Unhandled exception: %s' % parse_exception(e), None
finally:
close_mailbox()
if __name__ == "__main__":
exitcode, info, perf = main()
output_check_result(exitcode, info, perf)
sys.exit(exitcode)