#!/usr/local/bin/python import re from ipv4 import CIDR import sys, os import datetime import textwrap try: set; frozenset except NameError: import sets else: class sets: Set= set ImmutableSet= frozenset #import debug try: _= int(os.environ['COLUMNS']) except KeyError: _= 80 MAX_LENGTH= _-10 month_names= 'JanFebMarAprMayJunJulAugSepOctNovDec' # month_num converts from string to number (map) month_num= {} # month_name converts from number to string (list --directly indexed) month_name= [None] for ix in range(0, 36, 3): month_num[month_names[ix:ix+3]]= ix//3+1 month_name.append(month_names[ix:ix+3]) reInfo= re.compile("^(\d+(?:\.\d+){1,3})\s+(OK|REJECT)") reGetIP= re.compile("\[(\d+(?:\.\d+){3})\]") def extract_ip(text): match= reGetIP.search(text) if match: return explain_marked(match.group(1)) else: return '{???}' def explain_marked(ip): ip2, res= marked[ip] return "{%s=%s}" % (ip2, res) import platform if platform.uname()[1] in ("artemis.sil-tec.gr", "tril"): fi= file("/etc/postfix/clients_welcome") class dummy(dict): def _net_segments(key): offset= -1 while 1: yield key try: idx= key.rindex('.') except ValueError: break key= key[:idx] _net_segments= staticmethod(_net_segments) def __getitem__(self, key): for key in self._net_segments(key): try: return key, dict.__getitem__(self, key) except KeyError: pass else: return None, None def __contains__(self, key): return self[key][0] is not None marked= dummy() for line in fi: match= reInfo.match(line) if match: marked[match.group(1)]= intern(match.group(2)) fi.close() for line in open("/etc/hosts"): line= line.strip() if line.startswith('#') or not line: continue marked[line.split(None, 1)[0]]= "KNOWN_HOST" try: fp= open("/etc/postfix/grey_list", "r") except IOError: pass else: for line in fp: marked[line.rstrip('\n')]= "KNOWN_GREY" fp.close() else: class dummy: def __contains__(self, other): return False def __getitem__(self, key): return '' marked= dummy() del dummy reMatches= ( ('ipop3d-log', r'(?P\S+) ipop3d\[(?P\d+)\]: (?:Auth|(?:Autol|L)og(?:in|out)(?:\sfailure)?) user=(?P\S+) host=\S+ \S+' r'(?: nmsgs=\S+(?: ndele=(?P\d+))?)?'), ('smtpd-connect-2', # part2 of remote connection r'(?P\S+) postfix/smtpd\[(?P\d+)\]: ' r'(?P[^:]+): client=(?P[^[]+)\[(?P[^]]+)\]'), ('smtpd-connect', # start of remote connection r'(?P\S+) postfix/smtpd\[(?P\d+)\]: connect from (?P\S+)\[*(?P[^]]+)\]'), ('smtpd-disconnect', # end of remote connection r'(?P\S+) postfix/smtpd\[(?P\d+)\]: disconnect from (?P\S+)\[*(?P[^]]+)\]'), ('qmgr-from', # acknowledge message r'(?P\S+) postfix/qmgr\[(?P\d+)\]: (?P[^:]+): from=<(?P[^>]*)>, size=(?P\d+)'), ('smtp-to', # relay delivery r'(?P\S+) postfix/(?:qmgr|smtp)\[(?P\d+)\]: (?P[^:]+): to=<(?P[^>]+)>, ' r'(?:orig_to=<(?P[^>]+)>, )?' r'relay=(?P[^,]+),.*status=(?Psent|deferred|bounced) \((?P.+)\)'), ('cleanup', # associate queue_id with message_id r'(?P\S+)\spostfix/cleanup\[(?P\d+)\]:\s(?P[^:]+):\s(?:resent-)?message-id=<(?P[^>]+)>'), ('smtpd-reject', r'(?P\S+) postfix/smtpd\[(?P\d+)\]: (?P[^:]+): reject: ' r'(?P.+?\[(?P\d+\.\d+\.\d+\.\d+)\]:.+); (?:from=<(?P[^>]*)> )?to=<(?P[^>]+)>'), ('smtpd-lost-connection', r'(?P\S+) postfix/smtpd\[(?P\d+)\]: (?:lookup table|timeout after|lost connection)'), ('local-to', # local delivery r'(?P\S+) postfix/local\[(?P\d+)\]: (?P[^:]+): to=<(?P[^>]+)>, ' r'(?:orig_to=<(?P[^>]+)>, )?relay=(?P[^,]+),.*status=(?Psent|deferred|bounced) ' r'\((?P.+)\)'), ('procmail-ignore', r'(?P\S+) procmail\[(?P\d+)\]: Attempt to fake stamp'), ('smtpd-unknown-ip', r'(?P\S+) postfix/smtpd\[(?P\d+)\]: ' r'warning:.*(?:Host name has no address|Host not found|address not listed|database\s\S+\sis older|\S+ sent Message-ID:|Illegal address syntax)'), ('pickup', r'(?P\S+) postfix/pickup\[(?P\d+)\]: (?P[^:]+): uid=(?P\d+) from=<(?P[^>]+)>'), ('cleanup-reject', r'(?P\S+) postfix/cleanup\[(?P\d+)\]: (?P[^:]+): reject: ' r'(?P.+?(?P\[\d+\.\d+\.\d+\.\d+\]).*); from=<(?P[^>]+)?> to=<(?P[^>]+)>'), # artemis postfix/cleanup[24861]: warning: 2D0874635D: queue file size limit exceeded ('cleanup-size', r'(?P\S+) postfix/cleanup\[(?P\d+)\]:\swarning:\s(?P[^:]+): queue file size limit exceeded'), ('ipop3d-ignore', r'(?P\S+) ipop3d\[(?P\d+)\]: (?:Moved|Command stream end of file)'), # artemis postfix/smtp[3255]: warning: no MX host for littleswan.com has a valid A record ('smtp-to-ignore-mx-a', r'(?P\S+) postfix/smtp\[(?P\d+)\]: warning: (no MX host for|numeric domain name in resource data)'), ('smtp-to-delayed', r'(?P\S+) postfix/smtp\[(?P\d+)\]: connect to (?P[^:]+): ' r'(?:connect to|Connection timed out|read timeout|No route to host|server (?:refus|dropp)ed|Connection refused)'), ('smtp-to-ignore', r'(?P\S+) postfix/smtp\[(?P\d+)\]: (?P[^:]+): ' r'(?:enabling PIX)'), ('qmgr-expired', # message expired, return r'(?P\S+) postfix/qmgr\[(?P\d+)\]: (?P[^:]+): from=<(?P[^>]*)>, status=expired'), ('system-warning', r'(?P\S+) postfix/postfix-script: warning: (?P.*)'), ('system-info', r'(?P\S+) postfix/(?:master|postfix-script)(?:\[(?P\d+)\])?: (?P.*)'), ('ignore-1', r'(?P\S+) last message repeated'), ('ignore-2', r'(?P\S+) postfix/smtpd.*: Invalid argument$'), ('postsuper', r'(?P\S+) postfix/postsuper\[(?P\d+)\]:\s(?P[^:]+):\sremoved'), ('postsuper-ignore-1', r'(?P\S+) postfix/postsuper\[(?P\d+)\]:\sDeleted:'), ('smtpd-incoming-lost', # some message was lost r'(?P\S+) postfix/smtpd\[(?P\d+)\]: NOQUEUE: reject: MAIL from (?P\S+)\[(?P[^]]+)\]' r':\s(?P.*)'), ('ipop3d-ignore-1', r'(?P\S+) ipop3d\[(?P\d+)\]: (?:Error opening or locking|Connection reset by peer)'), ) def my_wrap(text): return '\n '.join(textwrap.wrap(text, MAX_LENGTH)) class Message(object): __slots__= 'message_id', 'from_whom', 'to_whom', 'size', 'data', 'selected', 'outgoing', 'timestamp', 'server', 'origin', 'ip' def __init__(self): self.selected= False self.data= [] self.outgoing= None self.origin= 'local' self.size= -1 self.ip= '127.0.0.1' def __str__(self): for ix in range(len(self.data)-1, -1, -1): if len(self.data[ix])> MAX_LENGTH: self.data[ix]= my_wrap(self.data[ix]) return '\n'.join(self.data) def main(username, file_object=sys.stdin): global_report = username == "_" if global_report: reLocal = re.compile(r'(?i)[^@]+(?:@(?:(?:marvin|artemis)?\.?sil-tec|shh|sanyo)\.gr)?$') admin_uids = 0, 501 current_user = os.getuid() warnings = [] IGNORE_POPPED= None, '0' at_username= username.lower()+"@" username= username.lower() last_date= [None] today= datetime.date.today() this_year= today.year def make_date(date_str): dt= datetime.datetime(this_year, month_num[date_str[:3]], int(date_str[4:6]), int(date_str[7:9]), int(date_str[10:12]), int(date_str[13:15])) if dt.date()>today: dt= dt.replace(year=this_year-1) return dt def optionally_print_date(dt): this_date= month_name[dt.month], dt.day if this_date!=last_date[0]: last_date[0]= this_date print "\n--- %s %02d ---" % this_date def is_user(*search_list): for item in search_list: item= item.lower() if global_report: if reLocal.match(item): return True elif item.startswith(at_username) or item==username: return True return False results= {None: 0} reList= [] for match_name, match_re_text in reMatches: try: reList.append( (match_name, re.compile(match_re_text)) ) except: print match_re_text raise results[match_name]= 0 # messages is a map of managed messages messages= {} messages['NOQUEUE']= Message() # output is the list of results output= [] # mess2queue is a map of message_id to queue_id mess2queue= {} # queue2queue is a map from queue_id to another queue_id through message_id queue2queue= {} counter= 0 for line in file_object: date_part, line= line[:15], line[16:] for match_name, match_re in reList: match= match_re.match(line) if match: results[match_name]+= 1 match_group= match.group break else: if current_user in admin_uids: if results[None]<16: sys.stderr.write('# ' + line) results[None]+= 1 # here process data if match_name=='ipop3d-log': if is_user(match_group('user')) and match_group('popped') not in IGNORE_POPPED: _= int(match_group('popped')) if global_report: output.append( (make_date(date_part), ' = %s %d popped message%s' % (match_group('user'), _, _!=1 and "s" or "") ) ) else: output.append( (make_date(date_part), ' = %d popped message%s' % (_, _!=1 and "s" or "") ) ) elif match_name=='smtpd-connect-2': queue_id= match_group('queue_id') if queue_id in queue2queue: queue_id= queue2queue[queue_id] try: _= messages[queue_id] except KeyError: _= messages[queue_id]= Message() _.timestamp= make_date(date_part) _.server= match_group('server')[:4] _.ip= match_group('remote_ip') _.origin= "%s [%s]" % (match_group('remote_name'), _.ip) elif match_name=='cleanup': message_id, queue_id= match_group('message_id'), match_group('queue_id') try: queue2queue[queue_id]= mess2queue[message_id] except KeyError: # we haven't seen this message_id yet mess2queue[message_id]= queue_id if queue_id not in messages: _= messages[queue_id]= Message() _.server= match_group('server')[:4] messages[queue_id].message_id= message_id elif match_name=='qmgr-from': queue_id, from_whom, size= match_group('queue_id'), match_group('from'), match_group('size') if queue_id in queue2queue: ## print "matched", queue_id, "to", queue2queue[queue_id] queue_id= queue2queue[queue_id] if queue_id in messages: _= messages[queue_id] else: _= messages[queue_id]= Message() messages[queue_id].server= match_group('server')[:4] # tzot 3>> _.from_whom= from_whom _.size= (int(size)+1023)//1024 if not hasattr(_, 'timestamp'): _.timestamp= make_date(date_part) if is_user(from_whom): if _.selected is not True: _.selected= True _.outgoing= True if global_report: _.data.append(" >> %s sent a %dK message [%s]" % (from_whom, _.size, _.server)) else: _.data.append(" >> You sent a %dK message [%s]" % (_.size, _.server)) elif match_name in ('local-to', 'smtp-to'): # local delivery queue_id, to_whom, status, remote_reply, relay= \ match_group('queue_id'), match_group('to'), match_group('status'), match_group('remote_reply'), \ match_group('relay') try: orig_to_whom= match_group('orig_to') except LookupError: orig_to_whom= None if queue_id in queue2queue: queue_id= queue2queue[queue_id] if queue_id in messages: # only if met already _=messages[queue_id] if _.selected and _.outgoing is True: if status=="sent": _.data.append("%s : %s (%s)" % (date_part[7:12], to_whom, relay=="local" and relay or remote_reply)) else: _.data.append("%s * %s %s: %s" % (date_part[7:12], to_whom, status, remote_reply)) elif is_user(to_whom) or orig_to_whom and is_user(orig_to_whom): if _.selected is not True: _.selected= True if global_report: try: _.from_whom except AttributeError: _.from_whom= "(missing!)" _.data.append("<< %s %dK message from %s @%s [%s] %s" % (to_whom, _.size, _.from_whom or '(postmaster)', _.origin, _.server, explain_marked(_.ip))) else: _.data.append("<< %dK message from %s @%s [%s] %s" % (_.size, _.from_whom or '(postmaster)', _.origin, _.server, explain_marked(_.ip))) elif match_name in ('smtpd-reject', 'cleanup-reject'): queue_id, from_whom, to_whom, reject_reason, ip= \ match_group('queue_id'), match_group('from'), match_group('to'), match_group('reject_reason'), match_group('ip') if queue_id == 'NOQUEUE': messages[queue_id].origin= "[%s]" % ip # XXX messages[queue_id].ip= ip try: _= messages[queue_id] except KeyError: # this is at the start of the log file, when logs rotated pass else: if is_user(to_whom) or global_report: if global_report: status= extract_ip(reject_reason) output.append( (make_date(date_part), my_wrap( "%s %s %.4s-reject from '%s' @%s: %s %s" % (3*('450' in reject_reason and '.' or '!'), to_whom, match_group('server'), from_whom, _.origin, reject_reason, explain_marked(_.ip))) ) ) else: status= extract_ip(reject_reason) output.append( (make_date(date_part), my_wrap( "%s %.4s-reject from '%s' @%s: %s %s" % (3*('450' in reject_reason and '.' or '!'), match_group('server'), from_whom, _.origin, reject_reason, explain_marked(_.ip))) ) ) elif match_name == 'cleanup-size': queue_id = match_group('queue_id') _= messages[queue_id] elif match_name == 'smtpd-incoming-lost': if global_report: # print >>sys.stderr, match_group('remote_ip'), match_group('remote_name') output.append( (make_date(date_part), my_wrap( "*** (unknown recipient) %.4s-reject from computer %s[%s]: %s %s" % (match_group('server'), match_group('remote_name'), match_group('remote_ip'), match_group('reason'), explain_marked(match_group('remote_ip'))) )) ) elif match_name == 'postsuper': # deleted by postmaster queue_id= match_group('queue_id') if queue_id in queue2queue: queue_id= queue2queue[queue_id] if queue_id in messages: # only if met already _=messages[queue_id] if _.selected and _.outgoing is True: _.data.append("%s *** removed from queue by postmaster@sil-tec.gr" % date_part[7:12]) # now sort the output list try: output.extend( [(x.timestamp, x) for x in messages.itervalues() if x.selected] ) except AttributeError: for x in messages.itervalues(): if x.selected: try: output.append( (x.timestamp, x) ) except AttributeError: print "problem in x", `x`, dir(x) output.sort() # and now report data last_hour_minute= None for dt, text in output: optionally_print_date(dt) if last_hour_minute == (dt.hour, dt.minute): print " %s" % text else: try: print "%02d:%02d %s" % (dt.hour, dt.minute, text) except IOError, exc: sys.stderr.write("***Failed with '%s'\n" % exc) break # last_hour_minute= dt.hour, dt.minute return # done here # temporary analysis -- should be commented later on results= zip(results.itervalues(), results.iterkeys()) results.sort() results.reverse() for match_count, match_name in results: print match_name, match_count try: import bind except ImportError: pass else: bind.bind_all(sys.modules[__name__])