#!/usr/bin/env python

'''
Test defined hosts for capability to receive messages
 for sending via SMTP protocol.

Sends simple keepalive messages with all the necessary data
 to establish 'keepalive session'. If an attempt to send message
 fails, it'll try to send error msg, or write it to a log, failing that.

Should be started with some fixed time interval, which should
 be (preferrably) passed to the script via '-i' parameter.

Sections of this script may look ugly, because some things are
 handled by custom wrapper libs in the original, but there's no
 point to drag them along just for one script.

Mike Kazantsev <mk.fraggod@gmail.com>
'''

from optparse import OptionParser
parser = OptionParser(usage='%prog [options] CMD')
parser.add_option(
	'-i', '--interval', action='store', type='int',
	dest='interval', metavar='MINUTES', default=60,
	help='interval, at which script gets launched'
)
optz, argz = parser.parse_args()

optz = dict(
	srv_list = dict(
		core_mailer='example.host',
		aux_mailer='relay.example.host'
	),
	# Human-friendly name of this probing server
	server = 'probe.example.host',
	# Address, checked for delivered msgs
	collector = 'probe mails collector <box@example.host>',
	# Probing server subject signature
	probe = 'Mail_KeepAlive',
	# Error reporting email, one address only
	report_to = 'admin@example.host',
	# Send ehlo or full message switch
	test_send = True,
	# Launch interval to pass on through messages
	interval = optz.interval*60,
	# Connection timeout, seconds, py2.6 only
	timeout = 10,
	# File to use if mail fails
	log = '/var/log/mail_test.log',
	# Spool file to store recent failures' info
	db = '/var/spool/mail_test_db'
)

#######

from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.Utils import formatdate
import smtplib


# Form the message
try: import json # py2.6+
except ImportError: import simplejson as json
from time import time, strftime, localtime

ts = int(time())
signature = 'Mail_KeepAlive to %s from %s/%s'%( '%(srv)s', optz['server'], strftime('%d.%m.%y %H:%M:%S', localtime(ts)) )
comm = dict(time=ts, interval=optz['interval'], server='%(srv)s', probe=optz['server'], report_to=optz['report_to'])
addr_from = 'mail_test@%s'%optz['server']

msg = MIMEMultipart()
msg['From'] = 'mail_test@%s <%s>'%(optz['server'], optz['report_to'])
msg['To'] = optz['collector']
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = signature
msg.attach( MIMEText('%s\n\n%s'%(signature, json.dumps(comm))) )
msg = msg.as_string()


# Send message
from cStringIO import StringIO as sio
import os, sys

import logging
log = logging.getLogger('core')
handler = logging.StreamHandler(open(optz['log'], 'a+'))
handler.setFormatter(logging.Formatter(
	'%(asctime)s %(levelname)s %(module)s.%(funcName)s: %(message)s',
	'(%d.%m.%y %H:%M:%S)'
))
log.addHandler(handler)
logging.basicConfig(level=logging.INFO)

import shelve, fcntl
try: fcntl.flock(optz['db'], fcntl.LOCK_EX)
except OSError: pass
db = shelve.open(optz['db'], writeback=True)


def debug_mail(err, debug):
	'''Forward error message w/ debug output to admin'''
	try:
		if srv in db['_reported']:
			log.info('Skipped duplicate error reporting to %s'%optz['report_to'])
			return
		else: db['_reported'].add(srv)
	except KeyError: db['_reported'] = set([srv])

	msg = MIMEMultipart()
	msg['From'] = addr_from
	msg['To'] = 'Postmaster <%s>'%optz['report_to']
	msg['Date'] = formatdate(localtime=True)
	msg['Subject'] = 'SMTP failure on %s'%srv
	msg.attach( MIMEText(
		'%s\n\n%s'%(
			'Error message: %s'%err,
			'Protocol exchange:\n%s'%debug
		)
	))

	link = smtplib.SMTP('localhost')
	link.set_debuglevel(1)
	link.sendmail(
		addr_from,
		optz['report_to'],
		msg.as_string()
	)
	link.quit()
	log.info('Sent error mail to %s'%optz.report_to)


for srv,link in optz['srv_list'].iteritems():
	dump = smtplib.stderr = sio()
	try:
		try: link = smtplib.SMTP(link, timeout=optz['timeout']) # Py 2.6 only
		except TypeError: link = smtplib.SMTP(link)
		link.set_debuglevel(1)
		if not optz['test_send']:
			try: link.ehlo_or_helo_if_needed() # Py 2.6 only
			except AttributeError: link.ehlo()
		else: link.sendmail(addr_from, optz['collector'], msg%dict(srv=srv))
		link.quit()
	except Exception, err:
		try: link.quit() # Try to terminate connection in a clean fashion
		except: pass
		dump.seek(0)
		debug = dump.read()
		try:
			dump = smtplib.stderr = sio()
			debug_mail(err, debug)
		except Exception, err_fatal:
			dump.seek(0)
			debug_fatal = dump.read()
			log.error('========== SMTP failure on %s: %s'%(srv, err))
			log.error('Protocol exchange:\n%s\n\n'%debug)
			log.fatal('===== Local SMTP failure (%s): %s'%(optz['server'], err_fatal))
			log.fatal('Protocol exchange:\n%s\n\n'%debug_fatal)
	else:
		try: db['_reported'].discard(srv)
		except KeyError: pass
