#!/usr/bin/python import sys if sys.hexversion < 0x2030300: raise ImportError('getmail version 4 requires Python version 2.3.3' ' or later') import os.path import time import ConfigParser import poplib import imaplib import pprint from optparse import OptionParser, OptionGroup import socket # Fredrik Steen # fri jul 7 15:32:27 CEST 2006 # Debian Bug: #290637 # This python bug is not going to be fixed. # https://sourceforge.net/tracker/?func=detail&atid=105470&aid=997100&group_id=5470 # Workaround for python2.3 to supress FutureWarnings from module optparse. if sys.hexversion < 0x02040000: import warnings try: warnings.filterwarnings(action='ignore', category=FutureWarning, module='optparse') except NameError: del warnings options_bool = ( 'read_all', 'delete', 'delivered_to', 'received', 'message_log_syslog', ) options_int = ( 'delete_after', 'max_message_size', 'max_messages_per_session', 'max_bytes_per_session', 'verbose', ) options_str = ( 'message_log', ) # Unix only try: import syslog except ImportError: pass # Fredrik Steen # fri jul 7 15:32:01 CEST 2006 # Add /usr/share/getmai4/getmailcore to getmail search path sys.path.append("/usr/share/getmail4") try: from getmailcore import __version__, retrievers, destinations, filters, \ logging from getmailcore.exceptions import * from getmailcore.utilities import eval_bool, logfile, format_params, \ address_no_brackets, expand_user_vars except ImportError, o: sys.stderr.write('ImportError: %s\n' % o) sys.exit(127) log = logging.Logger() log.addhandler(sys.stdout, logging.INFO, maxlevel=logging.INFO) log.addhandler(sys.stderr, logging.WARNING) defaults = { 'getmaildir' : '~/.getmail/', 'rcfile' : 'getmailrc', 'verbose' : 1, 'read_all' : True, 'delete' : False, 'delete_after' : 0, 'max_message_size' : 0, 'max_messages_per_session' : 0, 'max_bytes_per_session' : 0, 'delivered_to' : True, 'received' : True, 'message_log' : None, 'message_log_syslog' : False, 'logfile' : None, } ####################################### def blurb(): log.info('getmail version %s\n' % __version__) log.info('Copyright (C) 1998-2007 Charles Cazabon. Licensed under the' ' GNU GPL version 2.\n') ####################################### def go(configs): blurb() summary = [] for (configfile, retriever, _filters, destination, options) in configs: oplevel = options['verbose'] now = int(time.time()) msgs_retrieved = 0 bytes_retrieved = 0 msgs_skipped = 0 if options['message_log_syslog']: syslog.openlog('getmail', 0, syslog.LOG_MAIL) try: log.info('%s:\n' % retriever) logline = 'Initializing %s:' % retriever if options['logfile']: options['logfile'].write(logline) if options['message_log_syslog']: syslog.syslog(syslog.LOG_INFO, logline) retriever.initialize() destination.retriever_info(retriever) nummsgs = len(retriever) fmtlen = len(str(nummsgs)) for (msgnum, msgid) in enumerate(retriever): log.debug(' message %s ...\n' % msgid) msgnum += 1 retrieve = False reason = 'seen' delete = False timestamp = retriever.oldmail.get(msgid, None) size = retriever.getmsgsize(msgid) info = ('msg %*d/%*d (%d bytes)' % (fmtlen, msgnum, fmtlen, nummsgs, size)) logline = '%s msgid %s' % (info, msgid) if options['read_all'] or timestamp is None: retrieve = True if (options['max_message_size'] and size > options['max_message_size']): retrieve = False reason = 'oversized' if (options['max_bytes_per_session'] and (bytes_retrieved + size) > options['max_bytes_per_session']): retrieve = False reason = 'would surpass max_bytes_per_session' try: if retrieve: msg = retriever.getmsg(msgid) msgs_retrieved += 1 bytes_retrieved += size if oplevel > 1: info += (' from <%s>' % address_no_brackets(msg.sender)) if msg.recipient is not None: info += (' to <%s>' % address_no_brackets(msg.recipient)) logline += (' from <%s>' % address_no_brackets(msg.sender)) if msg.recipient is not None: logline += (' to <%s>' % address_no_brackets(msg.recipient)) for mail_filter in _filters: log.debug(' passing to filter %s\n' % mail_filter) msg = mail_filter.filter_message(msg, retriever) if msg is None: log.debug(' dropped by filter %s\n' % mail_filter) info += (' dropped by filter %s' % mail_filter) logline += (' dropped by filter %s' % mail_filter) retriever.delivered(msgid) break if msg is not None: r = destination.deliver_message(msg, options['delivered_to'], options['received']) log.debug(' delivered to %s\n' % r) info += ' delivered' if oplevel > 1: info += (' to %s' % r) logline += (' delivered to %s' % r) retriever.delivered(msgid) if options['delete']: delete = True else: msgs_skipped += 1 log.debug(' not retrieving (timestamp %s)\n' % timestamp) if oplevel > 1: info += ' not retrieved (%s)' % reason if (options['delete_after'] and timestamp and (now - timestamp) / 86400 >= options['delete_after']): log.debug(' older than %d days (%s seconds),' ' will delete\n' % (options['delete_after'], (now - timestamp))) delete = True if options['delete'] and timestamp: log.debug(' will delete\n') delete = True if not retrieve and timestamp is None: # We haven't retrieved this message. Don't delete it. log.debug(' not yet retrieved, not deleting\n') delete = False if delete: retriever.delmsg(msgid) log.debug(' deleted\n') info += ', deleted' logline += ', deleted' except getmailDeliveryError, o: log.error('Delivery error (%s)\n' % o) info += ', delivery error (%s)' % o if options['logfile']: options['logfile'].write('Delivery error (%s)' % o) except getmailFilterError, o: log.error('Filter error (%s)\n' % o) info += ', filter error (%s)' % o if options['logfile']: options['logfile'].write('Filter error (%s)' % o) if (retrieve or delete or oplevel > 1): log.info(' %s\n' % info) if retrieve or delete: if options['logfile']: options['logfile'].write(logline) if options['message_log_syslog']: syslog.syslog(syslog.LOG_INFO, logline) if (options['max_messages_per_session'] and msgs_retrieved >= options['max_messages_per_session']): log.debug('hit max_messages_per_session (%d), breaking\n' % options['max_messages_per_session']) if oplevel > 1: log.info(' max messages per session (%d)\n' % options['max_messages_per_session']) raise StopIteration('max_messages_per_session %d' % options['max_messages_per_session']) except StopIteration: pass except socket.timeout, o: retriever.write_oldmailfile(forget_deleted=False) if type(o) == tuple and len(o) > 1: o = o[1] log.error('%s: timeout (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('timeout error (%s)' % o) except (poplib.error_proto, imaplib.IMAP4.abort), o: retriever.write_oldmailfile(forget_deleted=False) log.error('%s: protocol error (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('protocol error (%s)' % o) except socket.gaierror, o: retriever.write_oldmailfile(forget_deleted=False) if type(o) == tuple and len(o) > 1: o = o[1] log.error('%s: error resolving name (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('gaierror error (%s)' % o) except socket.error, o: retriever.write_oldmailfile(forget_deleted=False) if type(o) == tuple and len(o) > 1: o = o[1] log.error('%s: socket error (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('socket error (%s)' % o) except getmailOperationError, o: retriever.write_oldmailfile(forget_deleted=False) log.error('%s: operation error (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('getmailOperationError error (%s)' % o) summary.append( (retriever, msgs_retrieved, bytes_retrieved, msgs_skipped) ) log.info(' %d messages (%d bytes) retrieved, %d skipped\n' % (msgs_retrieved, bytes_retrieved, msgs_skipped)) log.debug('retriever %s finished\n' % retriever) try: retriever.quit() except getmailOperationError, o: log.debug('%s: operation error during quit (%s)\n' % (configfile, o)) if options['logfile']: options['logfile'].write('%s: operation error during quit (%s)' % (configfile, o)) if sum([i for (unused, i, unused, unused) in summary]) and oplevel > 1: log.info('Summary:\n') for (retriever, msgs_retrieved, bytes_retrieved, unused) in summary: log.info('Retrieved %d messages (%s bytes) from %s\n' % (msgs_retrieved, bytes_retrieved, retriever)) ####################################### def main(): try: parser = OptionParser(version='%%prog %s' % __version__) parser.add_option('-g', '--getmaildir', dest='getmaildir', action='store', default=defaults['getmaildir'], help='look in DIR for config/data files', metavar='DIR') parser.add_option('-r', '--rcfile', dest='rcfile', action='append', default=[], help='load configuration from FILE (may be given multiple times)', metavar='FILE') parser.add_option('--dump', dest='dump_config', action='store_true', default=False, help='dump configuration and exit (debugging)') parser.add_option('--trace', dest='trace', action='store_true', default=False, help='print extended trace information (extremely verbose)') overrides = OptionGroup(parser, 'Overrides', 'The following options override those' ' specified in any getmailrc file.' ) overrides.add_option('-v', '--verbose', dest='override_verbose', action='count', help='operate more verbosely (may be given multiple times)') overrides.add_option('-q', '--quiet', dest='override_verbose', action='store_const', const=0, help='operate quietly (only report errors)') overrides.add_option('-d', '--delete', dest='override_delete', action='store_true', help='delete messages from server after retrieving') overrides.add_option('-l', '--dont-delete', dest='override_delete', action='store_false', help='do not delete messages from server after retrieving') overrides.add_option('-a', '--all', dest='override_read_all', action='store_true', help='retrieve all messages') overrides.add_option('-n', '--new', dest='override_read_all', action='store_false', help='retrieve only unread messages') parser.add_option_group(overrides) (options, args) = parser.parse_args(sys.argv[1:]) if args: raise getmailOperationError('unknown argument(s) %s ; try --help' % args) if options.trace: log.clearhandlers() if not options.rcfile: options.rcfile.append(defaults['rcfile']) s = '' for attr in dir(options): if attr.startswith('_'): continue if s: s += ',' s += '%s="%s"' % (attr, pprint.pformat(getattr(options, attr))) log.debug('parsed options: %s\n' % s) getmaildir_type = 'Default' if options.getmaildir != defaults['getmaildir']: getmaildir_type = 'Specified' getmaildir = expand_user_vars(options.getmaildir) if not os.path.exists(getmaildir): raise getmailOperationError( '%s config/data dir "%s" does not exist - create ' 'or specify alternate directory with --getmaildir option' % (getmaildir_type, getmaildir) ) if not os.path.isdir(getmaildir): raise getmailOperationError( '%s config/data dir "%s" is not a directory - fix ' 'or specify alternate directory with --getmaildir option' % (getmaildir_type, getmaildir) ) if not os.access(getmaildir, os.W_OK): raise getmailOperationError( '%s config/data dir "%s" is not writable - fix permissions ' 'or specify alternate directory with --getmaildir option' % (getmaildir_type, getmaildir) ) configs = [] for filename in options.rcfile: path = os.path.join(os.path.expanduser(options.getmaildir), filename) log.debug('processing rcfile %s\n' % path) if not os.path.exists(path): raise getmailOperationError('configuration file %s does' ' not exist' % path) elif not os.path.isfile(path): raise getmailOperationError('%s is not a file' % path) f = open(path, 'rb') config = { 'verbose' : defaults['verbose'], 'read_all' : defaults['read_all'], 'delete' : defaults['delete'], 'delete_after' : defaults['delete_after'], 'max_message_size' : defaults['max_message_size'], 'max_messages_per_session' : defaults['max_messages_per_session'], 'max_bytes_per_session' : defaults['max_bytes_per_session'], 'delivered_to' : defaults['delivered_to'], 'received' : defaults['received'], 'logfile' : defaults['logfile'], 'message_log' : defaults['message_log'], 'message_log_syslog' : defaults['message_log_syslog'], } # Python's ConfigParser .getboolean() couldn't handle booleans in # the defaults. Submitted a patch; they fixed it a different way. # But for the extant, unfixed versions, an ugly hack.... parserdefaults = config.copy() for (key, value) in parserdefaults.items(): if type(value) == bool: parserdefaults[key] = str(value) try: configparser = ConfigParser.RawConfigParser(parserdefaults) configparser.readfp(f, path) for option in options_bool: log.debug(' looking for option %s ... ' % option) if configparser.has_option('options', option): log.debug('got "%s"' % configparser.get('options', option)) try: config[option] = configparser.getboolean('options', option) log.debug('-> %s' % config[option]) except ValueError: raise getmailConfigurationError('configuration file' ' %s incorrect (option %s must be boolean,' ' not %s)' % (path, option, configparser.get('options', option))) else: log.debug('not found') log.debug('\n') for option in options_int: log.debug(' looking for option %s ... ' % option) if configparser.has_option('options', option): log.debug('got "%s"' % configparser.get('options', option)) try: config[option] = configparser.getint('options', option) log.debug('-> %s' % config[option]) except ValueError: raise getmailConfigurationError('configuration file' ' %s incorrect (option %s must be integer,' ' not %s)' % (path, option, configparser.get('options', option))) else: log.debug('not found') log.debug('\n') # Message log file for option in options_str: log.debug(' looking for option %s ... ' % option) if configparser.has_option('options', option): log.debug('got "%s"' % configparser.get('options', option)) config[option] = configparser.get('options', option) log.debug('-> %s' % config[option]) else: log.debug('not found') log.debug('\n') if config['message_log']: try: config['logfile'] = logfile(config['message_log']) except IOError, o: raise getmailConfigurationError('error opening' ' message_log file %s (%s)' % (config['message_log'], o)) # Clear out the ConfigParser defaults before processing further # sections configparser._defaults = {} # Retriever log.debug(' getting retriever\n') retriever_type = configparser.get('retriever', 'type') log.debug(' type="%s"\n' % retriever_type) retriever_func = getattr(retrievers, retriever_type) if not callable(retriever_func): raise getmailConfigurationError('configuration file %s' ' specifies incorrect retriever type (%s)' % (path, retriever_type)) retriever_args = { 'getmaildir' : options.getmaildir, 'configparser' : configparser, } for (name, value) in configparser.items('retriever'): if name in ('type', 'configparser'): continue if name == 'password': log.debug(' parameter %s=*\n' % name) else: log.debug(' parameter %s="%s"\n' % (name, value)) retriever_args[name] = value log.debug(' instantiating retriever %s with args %s\n' % (retriever_type, format_params(retriever_args))) try: retriever = retriever_func(**retriever_args) log.debug(' checking retriever configuration for %s\n' % retriever) retriever.checkconf() except getmailOperationError, o: log.error('Error initializing retriever: %s\n' % o) continue # Destination log.debug(' getting destination\n') destination_type = configparser.get('destination', 'type') log.debug(' type="%s"\n' % destination_type) destination_func = getattr(destinations, destination_type) if not callable(destination_func): raise getmailConfigurationError('configuration file %s' ' specifies incorrect destination type (%s)' % (path, destination_type)) destination_args = {'configparser' : configparser} for (name, value) in configparser.items('destination'): if name in ('type', 'configparser'): continue if name == 'password': log.debug(' parameter %s=*\n' % name) else: log.debug(' parameter %s="%s"\n' % (name, value)) destination_args[name] = value log.debug(' instantiating destination %s with args %s\n' % (destination_type, format_params(destination_args))) destination = destination_func(**destination_args) # Filters log.debug(' getting filters\n') _filters = [] filtersections = [section.lower() for section in configparser.sections() if section.lower().startswith('filter')] filtersections.sort() for section in filtersections: log.debug(' processing filter section %s\n' % section) filter_type = configparser.get(section, 'type') log.debug(' type="%s"\n' % filter_type) filter_func = getattr(filters, filter_type) if not callable(filter_func): raise getmailConfigurationError('configuration file %s' ' specifies incorrect filter type (%s)' % (path, filter_type)) filter_args = {'configparser' : configparser} for (name, value) in configparser.items(section): if name in ('type', 'configparser'): continue if name == 'password': log.debug(' parameter %s=*\n' % name) else: log.debug(' parameter %s="%s"\n' % (name, value)) filter_args[name] = value log.debug(' instantiating filter %s with args %s\n' % (filter_type, format_params(filter_args))) mail_filter = filter_func(**filter_args) _filters.append(mail_filter) except ConfigParser.NoSectionError, o: raise getmailConfigurationError('configuration file %s' ' missing section (%s)' % (path, o)) except ConfigParser.NoOptionError, o: raise getmailConfigurationError('configuration file %s' ' missing option (%s)' % (path, o)) except (ConfigParser.DuplicateSectionError, ConfigParser.InterpolationError, ConfigParser.MissingSectionHeaderError, ConfigParser.ParsingError), o: raise getmailConfigurationError('configuration file %s' ' incorrect (%s)' % (path, o)) except getmailConfigurationError, o: raise getmailConfigurationError('configuration file %s' ' incorrect (%s)' % (path, o)) # Apply overrides from commandline for option in ('read_all', 'delete', 'verbose'): val = getattr(options, 'override_%s' % option) if val is not None: log.debug('overriding option %s from commandline %s\n' % (option, val)) config[option] = val if config['verbose'] > 2: config['verbose'] = 2 if not options.trace and config['verbose'] == 0: log.clearhandlers() log.addhandler(sys.stderr, logging.WARNING) configs.append( (os.path.basename(filename), retriever, _filters, destination, config.copy()) ) if options.dump_config: blurb() for (filename, retriever, _filters, destination, config) in configs: log.info('getmail configuration:\n') log.info(' getmail version %s\n' % __version__) log.info(' Python version %s\n' % sys.version) log.info(' retriever: ') retriever.showconf() if _filters: for _filter in _filters: log.info(' filter: ') _filter.showconf() log.info(' destination: ') destination.showconf() log.info(' options:\n') names = config.keys() names.sort() for name in names: log.info(' %s : %s\n' % (name, config[name])) log.info('\n') sys.exit() # Go! go(configs) except KeyboardInterrupt: log.warning('Operation aborted by user (keyboard interrupt)\n') sys.exit(0) except getmailConfigurationError, o: log.error('Configuration error: %s\n' % o) sys.exit(2) except getmailOperationError, o: log.error('Error: %s\n' % o) sys.exit(3) except StandardError, o: log.critical('\nException: please read docs/BUGS and include the' ' following information in any bug report:\n\n') log.critical(' getmail version %s\n' % __version__) log.critical(' Python version %s\n\n' % sys.version) log.critical('Unhandled exception follows:\n') exc_type, value, tb = sys.exc_info() import traceback tblist = (traceback.format_tb(tb, None) + traceback.format_exception_only(exc_type, value)) if type(tblist) != list: tblist = [tblist] for line in tblist: log.critical(' %s\n' % line.rstrip()) log.critical('\nPlease also include configuration information' ' from running getmail\n') log.critical('with your normal options plus "--dump".\n') sys.exit(4) ####################################### if __name__ == '__main__': main()