diff options
| -rwxr-xr-x | fetchmail.py | 504 | 
1 files changed, 504 insertions, 0 deletions
diff --git a/fetchmail.py b/fetchmail.py new file mode 100755 index 00000000..ef3c5212 --- /dev/null +++ b/fetchmail.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python2 +# +# Python translation of fetchmail. +# Reads configuration from .fetchmailpyrc rather than .fetchmailrc +# +# Features removed: +# 1. Support for multiple usernames per UID. +# 2. Repolling on a changed rc file. +# 3. It's no longer possible to specify site parameters from the command line. + +VERSION = "X0.1" + +import os, sys, getpass, pwd, getopt, stat + +# fetchmail return status codes  +PS_SUCCESS	= 0	# successful receipt of messages +PS_NOMAIL       = 1	# no mail available +PS_SOCKET	= 2	# socket I/O woes +PS_AUTHFAIL	= 3	# user authorization failed +PS_PROTOCOL	= 4	# protocol violation +PS_SYNTAX	= 5	# command-line syntax error +PS_IOERR	= 6	# bad permissions on rc file +PS_ERROR	= 7	# protocol error +PS_EXCLUDE	= 8	# client-side exclusion error +PS_LOCKBUSY	= 9	# server responded lock busy +PS_SMTP         = 10      # SMTP error +PS_DNS		= 11	# fatal DNS error +PS_BSMTP	= 12	# output batch could not be opened +PS_MAXFETCH	= 13	# poll ended by fetch limit +PS_SERVBUSY	= 14	# server is busy +PS_IDLETIMEOUT	= 15	# timeout on imap IDLE +# leave space for more codes +PS_UNDEFINED	= 23	# something I hadn't thought of +PS_TRANSIENT	= 24	# transient failure (internal use) +PS_REFUSED	= 25	# mail refused (internal use) +PS_RETAINED	= 26	# message retained (internal use) +PS_TRUNCATED	= 27	# headers incomplete (internal use) + +# output noise level +O_SILENT	= 0	# mute, max squelch, etc. +O_NORMAL	= 1	# user-friendly +O_VERBOSE	= 2	# chatty +O_DEBUG		= 3	# prolix +O_MONITOR	= O_VERBOSE + +# magic port numbers +SMTP_PORT	= 25 +KPOP_PORT	= 1109 +SIMAP_PORT	= 993 +SPOP3_PORT	= 995 + +# authentication types +A_ANY		= 0	# use the first method that works +A_PASSWORD	= 1	# password authentication +A_NTLM		= 2	# Microsoft NTLM protocol +A_CRAM_MD5	= 3	# CRAM-MD5 shrouding (RFC2195) +A_OTP		= 4	# One-time password (RFC1508) +A_KERBEROS_V4	= 5	# authenticate w/ Kerberos V4 +A_KERBEROS_V5	= 6	# authenticate w/ Kerberos V5 +A_GSSAPI	= 7	# authenticate with GSSAPI +A_SSH		= 8	# authentication at session level + +def DOTLINE(s): +    return (s[0] == '.' and (s[1]=='\r' or s[1]=='\n' or s[1]=='\0')) + +# Error classes +class TransactionError(Exception): +    pass +class GeneralError(Exception): +    pass +class ProtocolError(Exception): +    pass + +class proto_pop2: +    "POP2 protocol methods" +    def __init__(self, ctl): +        name = 'POP2' +        service = 'pop2' +        sslservice = 'pop2' +        port = 109 +        sslport = 109 +        peek_capable = False +        tagged = False +        delimited = False +        repoll = False +        # Internal +        pound_arg = -1 +        equal_arg = -1 + +    def ack(sock): +        self.pound_arg = self.equal_arg = -1 +        buf = gen_recv(sock) +        if buf[0] == "#": +            pass +        elif buf[0] == "#": +            pound_arg = int(buf[1:]) +        elif buf[0] == '=': +            equal_arg = int(buf[1:]) +        elif buf[0] == '-': +            raise GeneralError() +        else: +            raise ProtocolError() +        return buf + +    def getauth(sock, ctl): +        shroud = ctl.password +        status = gen_transact(sock, \ +                              "HELO %s %s" % (ctl.remotename, ctl.password)) +        shroud = None +        return status + +    def getrange(sock, ctl, folder): +        if folder: +          ok = gen_transact(sock, "FOLD %s" % folder) +          if pound_arg == -1: +              raise GeneralError() +        else: +            # We should have picked up a count of messages in the user's +            # default inbox from the pop2_getauth() response.  +            # +            # Note: this logic only works because there is no way to select +            # both the unnamed folder and named folders within a single +            # fetchmail run.  If that assumption ever becomes invalid, the +            # pop2_getauth code will have to stash the pound response away +            # explicitly in case it gets stepped on. +          if pound_arg == -1: +              raise GeneralError() +        return(pound_arg, -1, -1) + +    def fetch(sock, ctl, number): +        # request nth message +        ok = gen_transact(sock, "READ %d", number); +        gen_send(sock, "RETR"); +        return equal_arg; + +    def trail(sock, ctl, number): +        # send acknowledgement for message data +        if ctl.keep: +            return gen_transact(sock, "ACKS") +        else: +            return gen_transact(sock, "ACKD") + +    def logout(sock, ctl): +        # send logout command +        return gen_transact(sock, "QUIT") + +class proto_pop3: +    "POP3 protocol methods" +    def __init__(self, ctl): +        name = 'POP3' +        service = 'pop2' +        sslservice = 'pop2' +        port = 110 +        sslport = 995 +        peek_capable = not ctl.fetchall +        tagged = False +        delimited = True +        retry = False +        # Internal +        has_gssapi = FALSE +        has_kerberos = FALSE +        has_cram = FALSE +        has_otp = FALSE +        has_ssl = FALSE + +        # FIXME: fill in POP3 logic + +class hostdata: +    "Per-mailserver control data." + +    # rc file data +    pollname = None		# poll label of host +    via = None			# "true" server name if non-NULL +    akalist = []		# server name first, then akas +    localdomains = []		# list of pass-through domains +    protocol = None		# protocol type +    service = None		# IPv6 service name +    netsec = None		# IPv6 security request +    port = 0			# TCP/IP service port number +    interval = 0		# cycles to skip between polls +    authenticate = 'A_PASSWD'	# authentication mode to try +    timeout = 300		# inactivity timout in seconds +    envelope = None		# envelope address list header +    envskip = 0			# skip to numbered envelope header +    qvirtual = None		# prefix removed from local user id +    skip = False		# suppress poll in implicit mode? +    dns	= True			# do DNS lookup on multidrop? +    uidl = False		# use RFC1725 UIDLs? +    sdps = False		# use Demon Internet SDPS *ENV +    checkalias = False     	# resolve aliases by comparing IPs? +    principal = None		# Kerberos principal for mail service +    esmtp_name = None		# ESMTP AUTH information +    esmtp_password = None + +    # Only used under Linux +    interface = None +    monitor = None +    monitor_io = 0 +    #struct interface_pair_s *interface_pair + +    plugin = None +    plugout = None + +    # computed for internal use +    base_protocol = None	# relevant protocol method table +    poll_count = 0		# count of polls so far +    queryname = None		# name to attempt DNS lookup on +    truename = None		# "true name" of server host +    trueaddr = None             # IP address of truename, as char +    lead_server = None		# ptr to lead query for this server +    esmtp_options = []		# ESMTP option list + +class query: +    "All the parameters of a fetchmail query." +    # mailserver connection controls +    server = None + +    # per-user data +    localnames = [] 		# including calling user's name +    wildcard = False		# should unmatched names be passed through +    remotename = None		# remote login name to use +    password = None		# remote password to use +    mailboxes = []		# list of mailboxes to check + +    # per-forwarding-target data +    smtphunt = []		# list of SMTP hosts to try forwarding to +    domainlist = []		# domainlist to fetch from +    smtpaddress = None		# address to force in RCPT TO  +    smtpname = None		# full RCPT TO name, including domain +    antispam = []		# list of listener's antispam response +    mda = None			# local MDA to pass mail to +    bsmtp = None		# BSMTP output file +    listener = 'SMTP'		# what's the listener's wire protocol? +    preconnect = None		# pre-connection command to execute +    postconnect = None		# post-connection command to execute + +    # per-user control flags +    keep = False		# if TRUE, leave messages undeleted +    fetchall = False		# if TRUE, fetch all (not just unseen) +    flush = False		# if TRUE, delete messages already seen +    rewrite = False		# if TRUE, canonicalize recipient addresses +    stripcr = False		# if TRUE, strip CRs in text +    forcecr = False		# if TRUE, force CRs before LFs in text +    pass8bits = False		# if TRUE, ignore Content-Transfer-Encoding +    dropstatus = False		# if TRUE, drop Status lines in mail +    dropdelivered = False	# if TRUE, drop Delivered-To lines in mail +    mimedecode = False		# if TRUE, decode MIME-armored messages +    idle = False		# if TRUE, idle after each poll +    limit = 0			# limit size of retrieved messages +    warnings = 3600		# size warning interval +    fetchlimit = 0		# max # msgs to get in single poll +    batchlimit = 0		# max # msgs to pass in single SMTP session +    expunge = 1			# max # msgs to pass between expunges +    use_ssl = False		# use SSL encrypted session +    sslkey = None		# optional SSL private key file +    sslcert = None		# optional SSL certificate file +    sslproto = None		# force usage of protocol (ssl2|ssl3|tls1) - defaults to ssl23 +    sslcertpath = None		# Trusted certificate directory for checking the server cert +    sslcertck = False		# Strictly check the server cert. +    sslfingerprint = None	# Fingerprint to check against +    properties = []		# passthrough properties for extensions +    tracepolls = False		# if TRUE, add poll trace info to Received + +    # internal use -- per-poll state +    active = False		# should we actually poll this server? +    destaddr = None		# destination host for this query +    errcount = 0		# count transient errors in last pass +    authfailcount = 0		# count of authorization failures +    wehaveauthed = 0		# We've managed to logon at least once! +    wehavesentauthnote = 0	# We've sent an authorization failure note +    wedged = 0			# wedged by auth failures or timeouts? +    smtphost = None		# actual SMTP host we connected to +    smtp_socket = -1		# socket descriptor for SMTP connection +    uid = 0			# UID of user to deliver to +    skipped = []		# messages skipped on the mail server +    oldsaved = [] +    newsaved = [] +    oldsavedend = [] +    lastid = None		# last Message-ID seen on this connection +    thisid = None		# Message-ID of current message + +    # internal use -- per-message state +    mimemsg = 0			# bitmask indicating MIME body-type +    digest = None + +    def mailbox_protocol(self): +         # We need to distinguish between mailbox and mailbag protocols. +         # Under a mailbox protocol we're pulling mail for a speecific user. +         # Under a mailbag protocol we're fetching mail for an entire domain. +         return self.server.protocol != proto_etrn + +if __name__ == '__main__': +    # C version queried FETCHMAILUSER, then USER, then LOGNAME. +    # Order here is FETCHMAILUSER, LOGNAME, USER, LNAME and USERNAME. +    user = os.getenv("FETCHMAILUSER") or getpass.getuser() +    for injector in ("QMAILINJECT", "NULLMAILER_FLAGS"): +        if os.getenv(injector): +            print >>sys.stderr, \ +                  ("fetchmail: The %s environment variable is set.\n" +                  "This is dangerous, as it can make qmail-inject or qmail's\n" +                  "sendmail wrapper tamper with your From or Message-ID " +                  "headers.\n" +                  "Try 'env %s= fetchmail YOUR ARGUMENTS HERE'\n") % (injector, injector) +            sys.exit(PS_UNDEFINED) + +    # Figure out who calling user is and where the run-control file is. +    # C version handled multiple usernames per PID; this doesn't. +    try: +        pwp = pwd.getpwuid(os.getuid()) +    except: +        print >>sys.stderr, "You don't exist.  Go away." +        sys.exit(PS_UNDEFINED) +    home = os.getenv("HOME") or pwp.pw_dir +    fmhome = os.getenv("FETCHMAILHOME") or home +    rcfile = os.path.join(fmhome, ".fetchmailpyrc") +    idfile = os.path.join(fmhome, ".fetchids") + +    cmdhelp = \ +	"usage:  fetchmail [options] [server ...]\n" \ +	"  Options are as follows:\n" \ +	"  -?, --help        display this option help\n" \ +	"  -V, --version     display version info\n" \ +	"  -c, --check       check for messages without fetching\n" \ +	"  -s, --silent      work silently\n" \ +	"  -v, --verbose     work noisily (diagnostic output)\n" \ +	"  -d, --daemon      run as a daemon once per n seconds\n" \ +	"  -N, --nodetach    don't detach daemon process\n" \ +	"  -q, --quit        kill daemon process\n" \ +	"  -f, --fetchmailrc specify alternate run control file\n" \ +	"  -a, --all         retrieve old and new messages\n" \ +	"  -k, --keep        save new messages after retrieval\n" \ +	"  -F, --flush       delete old messages from server\n" + +    # Now time to parse the command line +    try: +        (options, arguments) = getopt.getopt(sys.argv[1:], +                                             "?Vcsvd:NqfakF", +                                             ("help", +                                              "version", +                                              "check", +                                              "silent", +                                              "verbose", +                                              "daemon", +                                              "nodetach", +                                              "quit", +                                              "fetchmailrc", +                                              "all", +                                              "keep", +                                              "flush", +                                              )) +    except getopt.GetoptError: +        print cmdhelp +        sys.exit(PS_SYNTAX) +    versioninfo = checkonly = silent = nodetach = quitmode = False +    fetchall = keep = flutch = False  +    outlevel = O_NORMAL +    poll_interval = -1 +    for (switch, val) in options: +	if switch in ("-?", "--help"): +	    print cmdhelp +            sys.exit(0) +	elif switch in ("-V", "--version"): +	    versioninfo = True +	elif switch in ("-c", "--check"): +	    checkonly = True +	elif switch in ("-s", "--silent"): +	    outlevel = O_SILENT +	elif switch in ("-v", "--verbose"): +            if outlevel == O_VERBOSE: +                outlevel = O_DEBUG +            else: +                outlevel = O_VERBOSE +	elif switch in ("-d", "--daemon"): +            poll_interval = int(val) +	elif switch in ("-N", "--nodetach"): +	    outlevel = O_SILENT +	elif switch in ("-q", "--quitmode"): +	    quitmode = True +	elif switch in ("-f", "--fetchmailrc"): +	    rcfile = val +	elif switch in ("-a", "--all"): +	    fetchall = True +	elif switch in ("-k", "--keep"): +	    keep = True +	elif switch in ("-F", "--flush"): +	    flush = True + +        if versioninfo: +            print "This is fetchmail release", VERSION +            os.system("uname -a") + +        # avoid parsing the config file if all we're doing is killing a daemon +        fetchmailrc = {} +        if not quitmode or len(sys.argv) != 2: +            # user probably supplied a configuration file, check security +            if os.path.exists(rcfile): +                # the run control file must have the same uid as the +                # REAL uid of this process, it must have permissions +                # no greater than 600, and it must not be a symbolic +                # link.  We check these conditions here. +                try: +                    st = os.lstat(rcfile) +                except IOError: +                    sys.exit(PS_IOERR) +                if not versioninfo: +                    if not stat.S_ISREG(st.st_mode): +                            print >>sys.stderr, \ +                                  "File %s must be a regular file." % pathname; +                            sys.exit(PS_IOERR); + +                    if st.st_mode & 0067: +                            print >>sys.stderr, \ +                                  "File %s must have no more than -rwx--x--- (0710) permissions." % pathname; +                            sys.exit(PS_IOERR); +            # time to read the configuration +            if rcfile == '-': +                ifp = sys.stdin +            elif os.path.exists(rcfile): +                ifp = file(rcfile) +            try: +                exec ifp in globals() +            except SyntaxError: +                print >>sys.stderr, \ +                      "File %s is ill-formed." % pathname; +                sys.exit(PS_SYNTAX); +            ifp.close() +            # generate a default configuration if user did not supply one +            if not fetchmailrc: +                fetchmailrc = { +                    'poll_interval': 300, +                    "logfile": None, +                    "idfile": idfile, +                    "postmaster": "esr", +                    'bouncemail': True, +                    'spambounce': False, +                    "properties": "", +                    'invisible': False, +                    'showdots': False, +                    'syslog': False, +                    'servers': [] +                    } +                for site in arguments: +                    fetchmailrc['servers'].append({ +                        "pollname" : site, +                        'active' : False, +                        "via" : None, +                        "protocol" : "IMAP", +                        'port' : 0, +                        'timeout' : 300, +                        'interval' : 0, +                        "envelope" : "Received", +                        'envskip' : 0, +                        "qvirtual" : None, +                        "auth" : "any", +                        'dns' : True, +                        'uidl' : False, +                        "aka" : [], +                        "localdomains" : [], +                        "interface" : None, +                        "monitor" : None, +                        "plugin" : None, +                        "plugout" : None, +                        "principal" : None, +                        'tracepolls' : False, +                        'users' :  [ +                            { +                                "remote" : user, +                                "password" : None, +                                'localnames' : [user], +                                'fetchall' : False, +                                'keep' : False, +                                'flush' : False, +                                'rewrite' : True, +                                'stripcr' : True, +                                'forcecr' : False, +                                'pass8bits' : False, +                                'dropstatus' : False, +                                'dropdelivered' : False, +                                'mimedecode' : False, +                                'idle' : False, +                                "mda" : "/usr/bin/procmail -d %T", +                                "bsmtp" : None, +                                'lmtp' : False, +                                "preconnect" : None, +                                "postconnect" : None, +                                'limit' : 0, +                                'warnings' : 3600, +                                'fetchlimit' : 0, +                                'batchlimit' : 0, +                                'expunge' : 0, +                                "properties" : None, +                                "smtphunt" : ["localhost"], +                                "fetchdomains" : [], +                                "smtpaddress" : None, +                                "smtpname" : None, +                                'antispam' : '', +                                "mailboxes" : [], +                            } +                            ] +                    }) +            if poll_interval != -1:  +                fetchmailrc['poll_interval'] = poll_interval +            # now turn the configuration into control structures +  | 
