From c28fef7520bd1179a1a3a64fb8b1027bd9b802d0 Mon Sep 17 00:00:00 2001 From: "Eric S. Raymond" Date: Fri, 28 Mar 2003 18:36:05 +0000 Subject: Initial revision svn path=/trunk/; revision=3808 --- fetchmail.py | 504 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100755 fetchmail.py 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 + -- cgit v1.2.3