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