aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric S. Raymond <esr@thyrsus.com>2003-03-28 18:36:05 +0000
committerEric S. Raymond <esr@thyrsus.com>2003-03-28 18:36:05 +0000
commitc28fef7520bd1179a1a3a64fb8b1027bd9b802d0 (patch)
tree5293e1e6e033e2159468bbfb7ef6ca51008b562d
parent8ca72dc47017d3c98e75e940265ba25d50cd39d1 (diff)
downloadfetchmail-c28fef7520bd1179a1a3a64fb8b1027bd9b802d0.tar.gz
fetchmail-c28fef7520bd1179a1a3a64fb8b1027bd9b802d0.tar.bz2
fetchmail-c28fef7520bd1179a1a3a64fb8b1027bd9b802d0.zip
Initial revision
svn path=/trunk/; revision=3808
-rwxr-xr-xfetchmail.py504
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
+