diff options
-rw-r--r-- | NEWS | 16 | ||||
-rwxr-xr-x | fetchmailconf.py | 703 |
2 files changed, 413 insertions, 306 deletions
@@ -60,11 +60,26 @@ removed from a 6.5.0 or newer release.) * SSLv3 support may be removed from a future fetchmail release. It has been obsolete for many years and found insecure. Use TLS. * Fetchmailconf is deprecated and will be removed from a future release. +* Fetchmail does not guarantee compatibility with EOL OpenSSL versions. Support + for end-of-life OpenSSL versions may be removed even patchlevel releases. -------------------------------------------------------------------------------- fetchmail 6.4.2 (not yet released): +## BREAKING CHANGES: +* fetchmailconf now supports Python 3, but also requires Python 2.7.13 as + minimum version, but should only be used with 2.7.16 or newer (due to SSL + changes). Older Python versions may check SSL certificates not strictly + enough which will then cause complaints by fetchmail later. +* fetchmailconf now autoprobes SSL-wrapped connections (ports 993 and 995 for + IMAP and POP3) as well and by preference. +* fetchmailconf now defaults newly created users to "ssl" if either of the + existing users sets ssl, or if the server has freshly been probed and + found supporting ssl. + There is a caveat: adding a user to an existing server without probing it + again may skip adding ssl. (This does not prevent STARTTLS.) + ## BUG FIXES: * Fix three bugs in fetchmail.man (one unterminated string to .IP macro, one line that ran into a .PP macro, .TH date format), and remove one .br request from @@ -74,6 +89,7 @@ fetchmail 6.4.2 (not yet released): * When evaluating the need for STARTTLS in non-default configurations (SSL certificate validation turned off), fetchmail would only consider --sslproto tls1 as requiring STARTTLS, now all non-empty protocol versions do. +* fetchmailconf now properly writes "no sslcertck" if sslcertck is disabled. ## CHANGES: * Make t.smoke more robust and use temporary directory as FETCHMAILHOME, to make diff --git a/fetchmailconf.py b/fetchmailconf.py index 063e6f3d..7decc0ca 100755 --- a/fetchmailconf.py +++ b/fetchmailconf.py @@ -8,25 +8,38 @@ # WARNING: this needs to be updated for fetchmail 6.4's SSL options, # and other recent new options; -# WARNING: to be compatible with Python 3, needs to be run thru 2to3.py. -from __future__ import print_function - -version = "1.59" +from __future__ import print_function +from __future__ import division +from past.builtins import execfile +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import range +from past.utils import old_div +from builtins import object import sys - -MIN_PY = (2,3) +import time +import os +import socket +import getopt +import tempfile +import ssl +from tkinter import * +from tkinter.dialog import * + +VERSION = "1.60" + +MIN_PY = (2, 7, 13) if sys.version_info < MIN_PY: - sys.exit("fetchmailconf: Python %s.%s or later is required.\n" % MIN_PY); + sys.exit("fetchmailconf: Python %s.%s.%s or later is required.\n" % MIN_PY) -from Tkinter import * -from Dialog import * -import time, os, string, socket, getopt, tempfile +PY3K = sys.version_info >= (3,0) # # Define the data structures the GUIs will be tossing around # -class Configuration: +class Configuration(object): def __init__(self): self.poll_interval = 0 # Normally, run in foreground self.logfile = None # No logfile, initially @@ -52,47 +65,47 @@ class Configuration: ('invisible', 'Boolean')) def __repr__(self): - str = ""; + self_repr = "" if self.syslog != ConfigurationDefaults.syslog: - str = str + ("set syslog\n") + self_repr = self_repr + ("set syslog\n") elif self.logfile: - str = str + ("set logfile \"%s\"\n" % (self.logfile,)); + self_repr = self_repr + ("set logfile \"%s\"\n" % (self.logfile,)) if self.idfile != ConfigurationDefaults.idfile: - str = str + ("set idfile \"%s\"\n" % (self.idfile,)); + self_repr = self_repr + ("set idfile \"%s\"\n" % (self.idfile,)) if self.postmaster != ConfigurationDefaults.postmaster: - str = str + ("set postmaster \"%s\"\n" % (self.postmaster,)); + self_repr = self_repr + ("set postmaster \"%s\"\n" % (self.postmaster,)) if self.bouncemail: - str = str + ("set bouncemail\n") + self_repr = self_repr + ("set bouncemail\n") else: - str = str + ("set nobouncemail\n") + self_repr = self_repr + ("set nobouncemail\n") if self.spambounce: - str = str + ("set spambounce\n") + self_repr = self_repr + ("set spambounce\n") else: - str = str + ("set no spambounce\n") + self_repr = self_repr + ("set no spambounce\n") if self.softbounce: - str = str + ("set softbounce\n") + self_repr = self_repr + ("set softbounce\n") else: - str = str + ("set no softbounce\n") + self_repr = self_repr + ("set no softbounce\n") if self.properties != ConfigurationDefaults.properties: - str = str + ("set properties \"%s\"\n" % (self.properties,)); + self_repr = self_repr + ("set properties \"%s\"\n" % (self.properties,)) if self.poll_interval > 0: - str = str + "set daemon " + repr(self.poll_interval) + "\n" + self_repr = self_repr + "set daemon " + repr(self.poll_interval) + "\n" if self.invisible: - str = str + ("set invisible\n") + self_repr = self_repr + ("set invisible\n") for site in self.servers: - str = str + repr(site) - return str + self_repr = self_repr + repr(site) + return self_repr def __delitem__(self, name): - for si in range(len(self.servers)): - if self.servers[si].pollname == name: - del self.servers[si] + for i in range(len(self.servers)): + if self.servers[i].pollname == name: + del self.servers[i] break def __str__(self): return "[Configuration: " + repr(self) + "]" -class Server: +class Server(object): def __init__(self): self.pollname = None # Poll label self.via = None # True name of host @@ -119,6 +132,9 @@ class Server: self.tracepolls = FALSE # Add trace-poll info to headers self.badheader = FALSE # Pass messages with bad headers on? self.users = [] # List of user entries for site + + self.ssldefault = False # this is a helper for autoprobing to initialize user defaults + Server.typemap = ( ('pollname', 'String'), ('via', 'String'), @@ -143,15 +159,14 @@ class Server: ('esmtppassword', 'String'), ('principal', 'String'), ('tracepolls','Boolean'), - ('badheader', 'Boolean')) + ('badheader', 'Boolean'), + ('ssldefault','Boolean')) def dump(self, folded): - res = "" - if self.active: res = res + "poll" - else: res = res + "skip" - res = res + (" " + self.pollname) + res = "poll" if self.active else "skip" + res += " " + self.pollname if self.via: - res = res + (" via " + str(self.via) + "\n"); + res = res + (" via " + str(self.via) + "\n") if self.protocol != ServerDefaults.protocol: res = res + " with proto " + self.protocol if self.service and self.protocol and self.service != defaultports[self.protocol] and defaultports[self.protocol] and self.service != ianaservices[defaultports[self.protocol]]: @@ -166,7 +181,7 @@ class Server: else: res = res + " envelope " + self.envelope if self.qvirtual: - res = res + (" qvirtual " + str(self.qvirtual) + "\n"); + res = res + (" qvirtual " + str(self.qvirtual) + "\n") if self.auth != ServerDefaults.auth: res = res + " auth " + self.auth if self.dns != ServerDefaults.dns or self.uidl != ServerDefaults.uidl: @@ -175,26 +190,24 @@ class Server: res = res + flag2str(self.dns, 'dns') if self.uidl != ServerDefaults.uidl: res = res + flag2str(self.uidl, 'uidl') - if folded: res = res + "\n " - else: res = res + " " + res += "\n " if folded else " " if self.aka: - res = res + "aka" - for x in self.aka: - res = res + " " + x - if self.aka and self.localdomains: res = res + " " + res = res + "aka " + " ".join(self.aka) + if self.aka and self.localdomains: + res = res + " " if self.localdomains: - res = res + ("localdomains") - for x in self.localdomains: + res = res + ("localdomains") + for x in self.localdomains: res = res + " " + x - if (self.aka or self.localdomains): + if self.aka or self.localdomains: if folded: res = res + "\n " else: res = res + " " if self.tracepolls: - res = res + "tracepolls\n" + res = res + "tracepolls\n" if self.interface: res = res + " interface " + str(self.interface) @@ -214,14 +227,15 @@ class Server: if folded: res = res + "\n" if self.badheader: - res = res + "bad-header accept " + res = res + "bad-header accept " - if res[-1] == " ": res = res[0:-1] + if res[-1] == " ": + res = res[0:-1] - for user in self.users: - res = res + repr(user) + for i in self.users: + res = res + repr(i) res = res + "\n" - return res; + return res def __delitem__(self, name): for ui in range(len(self.users)): @@ -235,7 +249,7 @@ class Server: def __str__(self): return "[Server: " + self.dump(FALSE) + "]" -class User: +class User(object): def __init__(self): if "USER" in os.environ: self.remote = os.environ["USER"] # Remote username @@ -280,7 +294,7 @@ class User: self.sslkey = None # SSL key filename self.sslcert = None # SSL certificate filename self.sslproto = None # Force SSL? - self.sslcertck = 1 # Enable strict SSL cert checking + self.sslcertck = True # Enable strict SSL cert checking self.sslcertpath = None # Path to trusted certificates self.sslcommonname = None # SSL CommonName to expect self.sslfingerprint = None # SSL key fingerprint to check @@ -328,7 +342,7 @@ class User: def __repr__(self): res = " " - res = res + "user " + repr(self.remote) + " there "; + res = res + "user " + repr(self.remote) + " there " if self.password: res = res + "with password " + repr(self.password) + " " if self.localnames: @@ -393,7 +407,7 @@ class User: res = res + " sslcert " + repr(self.sslcert) if self.sslproto and self.sslproto != UserDefaults.sslproto: res = res + " sslproto " + repr(self.sslproto) - if self.sslcertck and self.sslcertck != UserDefaults.sslcertck: + if self.sslcertck is not None and self.sslcertck != UserDefaults.sslcertck: res = res + flag2str(self.sslcertck, 'sslcertck') if self.sslcertpath and self.sslcertpath != UserDefaults.sslcertpath: res = res + " sslcertpath " + repr(self.sslcertpath) @@ -404,7 +418,7 @@ class User: if self.expunge != UserDefaults.expunge: res = res + " expunge " + repr(self.expunge) res = res + "\n" - trimmed = self.smtphunt; + trimmed = self.smtphunt if trimmed != [] and trimmed[len(trimmed) - 1] == "localhost": trimmed = trimmed[0:len(trimmed) - 1] if trimmed != [] and trimmed[len(trimmed) - 1] == hostname: @@ -423,10 +437,10 @@ class User: res = res + " " + x res = res + "\n" if self.mailboxes: - res = res + " folder" - for x in self.mailboxes: + res = res + " folder" + for x in self.mailboxes: res = res + ' "%s"' % x - res = res + "\n" + res = res + "\n" for fld in ('smtpaddress', 'preconnect', 'postconnect', 'mda', 'bsmtp', 'properties'): if getattr(self, fld): res = res + " %s %s\n" % (fld, repr(getattr(self, fld))) @@ -434,7 +448,7 @@ class User: res = res + flag2str(self.lmtp, 'lmtp') if self.antispam != UserDefaults.antispam: res = res + " antispam " + self.antispam + "\n" - return res; + return res def __str__(self): return "[User: " + repr(self) + "]" @@ -451,6 +465,8 @@ ianaservices = {"pop2":109, "smtp":25, "odmr":366} +sslservices = { "pop3":995, "imap":993 } + # fetchmail protocol to IANA service name defaultports = {"auto":None, "POP2":"pop2", @@ -473,66 +489,67 @@ You must select an item in the list box (by clicking on it). def flag2str(value, string): # make a string representation of a .fetchmailrc flag or negated flag - str = "" + res = "" if value != None: - str = str + (" ") - if value == FALSE: str = str + ("no ") - str = str + string; - return str + res = res + (" ") + if value == FALSE: + res = res + ("no ") + res = res + string + return res class LabeledEntry(Frame): # widget consisting of entry field with caption to left def bind(self, key, action): - self.E.bind(key, action) + self.entry.bind(key, action) def focus_set(self): - self.E.focus_set() + self.entry.focus_set() def __init__(self, Master, text, textvar, lwidth, ewidth=12): Frame.__init__(self, Master) - self.L = Label(self, {'text':text, 'width':lwidth, 'anchor':'w'}) - self.E = Entry(self, {'textvar':textvar, 'width':ewidth}) - self.L.pack({'side':'left'}) - self.E.pack({'side':'left', 'expand':'1', 'fill':'x'}) + self.label = Label(self, {'text':text, 'width':lwidth, 'anchor':'w'}) + self.entry = Entry(self, {'textvar':textvar, 'width':ewidth}) + self.label.pack({'side':'left'}) + self.entry.pack({'side':'left', 'expand':'1', 'fill':'x'}) def ButtonBar(frame, legend, ref, alternatives, depth, command): # array of radio buttons, caption to left, picking from a string list - bar = Frame(frame) - width = (len(alternatives)+1) / depth; - Label(bar, text=legend).pack(side=LEFT) + bbar = Frame(frame) + width = old_div((len(alternatives)+1), depth) + Label(bbar, text=legend).pack(side=LEFT) for column in range(width): - subframe = Frame(bar) + subframe = Frame(bbar) for row in range(depth): ind = width * row + column if ind < len(alternatives): Radiobutton(subframe, - {'text':alternatives[ind], - 'variable':ref, - 'value':alternatives[ind], - 'command':command}).pack(side=TOP, anchor=W) + {'text':alternatives[ind], + 'variable':ref, + 'value':alternatives[ind], + 'command':command}).pack(side=TOP, anchor=W) else: # This is just a spacer Radiobutton(subframe, - {'text':" ",'state':DISABLED}).pack(side=TOP, anchor=W) + {'text':" ",'state':DISABLED}).pack(side=TOP, anchor=W) subframe.pack(side=LEFT) - bar.pack(side=TOP); - return bar + bbar.pack(side=TOP) + return bbar def helpwin(helpdict): # help message window with a self-destruct button - helpwin = Toplevel() - helpwin.title(helpdict['title']) - helpwin.iconname(helpdict['title']) - Label(helpwin, text=helpdict['banner']).pack() - textframe = Frame(helpwin) + h_w = Toplevel() + h_w.title(helpdict['title']) + h_w.iconname(helpdict['title']) + Label(h_w, text=helpdict['banner']).pack() + textframe = Frame(h_w) scroll = Scrollbar(textframe) - helpwin.textwidget = Text(textframe, setgrid=TRUE) + h_w.textwidget = Text(textframe, setgrid=TRUE) textframe.pack(side=TOP, expand=YES, fill=BOTH) - helpwin.textwidget.config(yscrollcommand=scroll.set) - helpwin.textwidget.pack(side=LEFT, expand=YES, fill=BOTH) - scroll.config(command=helpwin.textwidget.yview) + h_w.textwidget.config(yscrollcommand=scroll.set) + h_w.textwidget.pack(side=LEFT, expand=YES, fill=BOTH) + scroll.config(command=h_w.textwidget.yview) scroll.pack(side=RIGHT, fill=BOTH) - helpwin.textwidget.insert(END, helpdict['text']); - Button(helpwin, text='Done', - command=lambda x=helpwin: x.destroy(), bd=2).pack() + h_w.textwidget.insert(END, helpdict['text']) + Button(h_w, text='Done', + command=lambda x=h_w: x.destroy(), bd=2).pack() textframe.pack(side=TOP) def make_icon_window(base, image): @@ -546,15 +563,16 @@ def make_icon_window(base, image): # scope when the enclosing function returns. Therefore # we have to explicitly link them to something. base.keepalive.append(icon_image) - except: + except Exception: pass class ListEdit(Frame): # edit a list of values (duplicates not allowed) with a supplied editor hook - def __init__(self, newlegend, list, editor, deletor, master, helptxt): + def __init__(self, newlegend, list_cont, editor, deletor, master, helptxt): + Frame.__init__(self, master) self.editor = editor self.deletor = deletor - self.list = list + self.list_cont = list_cont # Set up a widget to accept new elements self.newval = StringVar(master) @@ -567,19 +585,19 @@ class ListEdit(Frame): listframe = Frame(master) scroll = Scrollbar(listframe) self.listwidget = Listbox(listframe, height=0, selectmode='browse') - if self.list: - for x in self.list: - self.listwidget.insert(END, x) + if self.list_cont: + for i in self.list_cont: + self.listwidget.insert(END, i) listframe.pack(side=TOP, expand=YES, fill=BOTH) self.listwidget.config(yscrollcommand=scroll.set) self.listwidget.pack(side=LEFT, expand=YES, fill=BOTH) scroll.config(command=self.listwidget.yview) scroll.pack(side=RIGHT, fill=BOTH) self.listwidget.config(selectmode=SINGLE, setgrid=TRUE) - self.listwidget.bind('<Double-1>', self.handleList); - self.listwidget.bind('<Return>', self.handleList); + self.listwidget.bind('<Double-1>', self.handleList) + self.listwidget.bind('<Return>', self.handleList) - bf = Frame(master); + bf = Frame(master) if self.editor: Button(bf, text='Edit', command=self.editItem).pack(side=LEFT) Button(bf, text='Delete', command=self.deleteItem).pack(side=LEFT) @@ -592,16 +610,17 @@ class ListEdit(Frame): def help(self): helpwin(self.helptxt) - def handleList(self, event): - self.editItem(); + def handleList(self, unused): + self.editItem() - def handleNew(self, event): + def handleNew(self, unused): item = self.newval.get() if item: - entire = self.listwidget.get(0, self.listwidget.index('end')); + entire = self.listwidget.get(0, self.listwidget.index('end')) if item and (not entire) or (not item in self.listwidget.get(0, self.listwidget.index('end'))): self.listwidget.insert('end', item) - if self.list != None: self.list.append(item) + if self.list_cont != None: + self.list_cont.append(item) if self.editor: self.editor(*(item,)) self.newval.set('') @@ -613,7 +632,7 @@ class ListEdit(Frame): else: index = int(select[0]) if self.editor: - label = self.listwidget.get(index); + label = self.listwidget.get(index) if self.editor: self.editor(*(label,)) @@ -623,10 +642,10 @@ class ListEdit(Frame): helpwin(listboxhelp) else: index = int(select[0]) - label = self.listwidget.get(index); + label = self.listwidget.get(index) self.listwidget.delete(index) - if self.list != None: - del self.list[index] + if self.list_cont != None: + del self.list_cont[index] if self.deletor != None: self.deletor(*(label,)) @@ -639,7 +658,7 @@ def ConfirmQuit(frame, context): default = 1) return ans.num == 0 -def dispose_window(master, legend, help, savelegend='OK'): +def dispose_window(master, legend, help_text, savelegend='OK'): dispose = Frame(master, relief=RAISED, bd=5) Label(dispose, text=legend).pack(side=TOP,pady=10) Button(dispose, text=savelegend, fg='blue', @@ -647,11 +666,11 @@ def dispose_window(master, legend, help, savelegend='OK'): Button(dispose, text='Quit', fg='blue', command=master.nosave).pack(side=LEFT) Button(dispose, text='Help', fg='blue', - command=lambda x=help: helpwin(x)).pack(side=RIGHT) + command=lambda x=help_text: helpwin(x)).pack(side=RIGHT) dispose.pack(fill=X) return dispose -class MyWidget: +class MyWidget(object): # Common methods for Tkinter widgets -- deals with Tkinter declaration def post(self, widgetclass, field): for x in widgetclass.typemap: @@ -779,11 +798,15 @@ This will take you to a site configuration dialogue. class ConfigurationEdit(Frame, MyWidget): def __init__(self, configuration, outfile, master, onexit): + Frame.__init__(self, master) + MyWidget.__init__(self) self.subwidgets = {} self.configuration = configuration self.outfile = outfile self.container = master self.onexit = onexit + self.keepalive = [] # Use this to anchor the PhotoImage object + self.mode = None # Novice or Expert ConfigurationEdit.mode_to_help = { 'novice':configure_novice_help, 'expert':configure_expert_help } @@ -793,17 +816,17 @@ class ConfigurationEdit(Frame, MyWidget): def server_delete(self, sitename): try: - for user in self.subwidgets.keys(): - user.destruct() + for user_it in list(self.subwidgets.keys()): + user_it.destruct() del self.configuration[sitename] - except: - pass + except e: + print("Exception discarded in ConfigurationEdit.server_delete(): {}".format(e)) def edit(self, mode): self.mode = mode Frame.__init__(self, self.container) - self.master.title('fetchmail ' + self.mode + ' configurator'); - self.master.iconname('fetchmail ' + self.mode + ' configurator'); + self.master.title('fetchmail ' + self.mode + ' configurator') + self.master.iconname('fetchmail ' + self.mode + ' configurator') self.master.protocol('WM_DELETE_WINDOW', self.nosave) self.keepalive = [] # Use this to anchor the PhotoImage object make_icon_window(self, fetchmail_icon) @@ -817,8 +840,8 @@ class ConfigurationEdit(Frame, MyWidget): gf = Frame(self, relief=RAISED, bd = 5) Label(gf, - text='Fetchmail Run Controls', - bd=2).pack(side=TOP, pady=10) + text='Fetchmail Run Controls', + bd=2).pack(side=TOP, pady=10) df = Frame(gf) @@ -838,38 +861,38 @@ class ConfigurationEdit(Frame, MyWidget): if self.mode != 'novice': pf = Frame(gf) Checkbutton(pf, - {'text':'Bounces to sender?', - 'variable':self.bouncemail, - 'relief':GROOVE}).pack(side=LEFT, anchor=W) + {'text':'Bounces to sender?', + 'variable':self.bouncemail, + 'relief':GROOVE}).pack(side=LEFT, anchor=W) pf.pack(fill=X) sb = Frame(gf) Checkbutton(sb, - {'text':'Send spam bounces?', - 'variable':self.spambounce, - 'relief':GROOVE}).pack(side=LEFT, anchor=W) + {'text':'Send spam bounces?', + 'variable':self.spambounce, + 'relief':GROOVE}).pack(side=LEFT, anchor=W) sb.pack(fill=X) sb = Frame(gf) Checkbutton(sb, - {'text':'Treat permanent errors as temporary?', - 'variable':self.softbounce, - 'relief':GROOVE}).pack(side=LEFT, anchor=W) + {'text':'Treat permanent errors as temporary?', + 'variable':self.softbounce, + 'relief':GROOVE}).pack(side=LEFT, anchor=W) sb.pack(fill=X) sf = Frame(gf) Checkbutton(sf, - {'text':'Log to syslog?', - 'variable':self.syslog, - 'relief':GROOVE}).pack(side=LEFT, anchor=W) + {'text':'Log to syslog?', + 'variable':self.syslog, + 'relief':GROOVE}).pack(side=LEFT, anchor=W) log = LabeledEntry(sf, ' Logfile:', self.logfile, '14') log.pack(side=RIGHT, anchor=E) sf.pack(fill=X) Checkbutton(gf, - {'text':'Invisible mode?', - 'variable':self.invisible, - 'relief':GROOVE}).pack(side=LEFT, anchor=W) + {'text':'Invisible mode?', + 'variable':self.invisible, + 'relief':GROOVE}).pack(side=LEFT, anchor=W) # Set the idfile log = LabeledEntry(gf, ' Idfile:', self.idfile, '14') log.pack(side=RIGHT, anchor=E) @@ -882,14 +905,14 @@ class ConfigurationEdit(Frame, MyWidget): text='Remote Mail Server Configurations', bd=2).pack(side=TOP, pady=10) ListEdit('New Server:', - map(lambda x: x.pollname, self.configuration.servers), - lambda site, self=self: self.server_edit(site), - lambda site, self=self: self.server_delete(site), - lf, remotehelp) + [i.pollname for i in self.configuration.servers], + lambda site, self=self: self.server_edit(site), + lambda site, self=self: self.server_delete(site), + lf, remotehelp) lf.pack(fill=X) def destruct(self): - for sitename in self.subwidgets.keys(): + for sitename in list(self.subwidgets.keys()): self.subwidgets[sitename].destruct() self.master.destroy() self.onexit() @@ -899,18 +922,19 @@ class ConfigurationEdit(Frame, MyWidget): self.destruct() def save(self): - for sitename in self.subwidgets.keys(): - self.subwidgets[sitename].save() + for i in self.subwidgets: + i.save() self.fetch(Configuration, 'configuration') fm = None if not self.outfile: fm = sys.stdout - elif not os.path.isfile(self.outfile) or Dialog(self, - title = 'Overwrite existing run control file?', - text = 'Really overwrite existing run control file?', - bitmap = 'question', - strings = ('Yes', 'No'), - default = 1).num == 0: + elif (not os.path.isfile(self.outfile) + or Dialog(self, + title = 'Overwrite existing run control file?', + text = 'Really overwrite existing run control file?', + bitmap = 'question', + strings = ('Yes', 'No'), + default = 1).num == 0): try: os.rename(self.outfile, self.outfile + "~") # Pre-1.5.2 compatibility... @@ -923,7 +947,7 @@ class ConfigurationEdit(Frame, MyWidget): # be paranoid if fm != sys.stdout: os.chmod(self.outfile, 0o600) - fm.write("# Configuration created %s by fetchmailconf %s\n" % (time.ctime(time.time()), version)) + fm.write("# Configuration created %s by fetchmailconf %s\n" % (time.ctime(time.time()), VERSION)) fm.write(repr(self.configuration)) if self.outfile: fm.close() @@ -1073,25 +1097,63 @@ you will open a window to configure the user's options on that site. """} +def get_greetline(_hostname, port, sslmode): + sock = None + greetline = None + address = None + errors = [] + for res in socket.getaddrinfo(_hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + sock = socket.socket(af, socktype, proto) + sock.settimeout(5) + except socket.error as msg: + errors.append("socket({}, {}, {}): {}".format(repr(af), repr(socktype), repr(proto), msg)) + sock = None + continue + try: + if sslmode: + context = ssl.create_default_context() + conn = context.wrap_socket(sock, server_hostname = canonname or _hostname) + else: + conn = sock + conn.connect(sa) + greetline = conn.recv(1024) + if PY3K: + greetline = greetline.decode('us-ascii','replace') + conn.shutdown(socket.SHUT_RDWR) + conn.close() + address = sa + except (socket.error, ssl.SSLError) as msg: + errors.append("{} port {}: {}".format(sa[0], sa[1], msg)) + sock.close() + sock = None + continue + break + return greetline, address, errors + class ServerEdit(Frame, MyWidget): def __init__(self, host, parent): + Frame.__init__(self) + MyWidget.__init__(self) self.parent = parent self.server = None self.subwidgets = {} + self.keepalive = [] # Use this to anchor the PhotoImage object for site in parent.configuration.servers: if site.pollname == host: self.server = site - if (self.server == None): - self.server = Server() - self.server.pollname = host - self.server.via = None - parent.configuration.servers.append(self.server) + if self.server is None: + self.server = Server() + self.server.pollname = host + self.server.via = None + parent.configuration.servers.append(self.server) def edit(self, mode, master=None): Frame.__init__(self, master) Pack.config(self) - self.master.title('Fetchmail host ' + self.server.pollname); - self.master.iconname('Fetchmail host ' + self.server.pollname); + self.master.title('Fetchmail host ' + self.server.pollname) + self.master.iconname('Fetchmail host ' + self.server.pollname) self.post(Server, 'server') self.makeWidgets(self.server.pollname, mode) self.keepalive = [] # Use this to anchor the PhotoImage object @@ -1102,7 +1164,7 @@ class ServerEdit(Frame, MyWidget): return self def destruct(self): - for username in self.subwidgets.keys(): + for username in list(self.subwidgets.keys()): self.subwidgets[username].destruct() del self.parent.subwidgets[self.server.pollname] self.master.destroy() @@ -1113,8 +1175,8 @@ class ServerEdit(Frame, MyWidget): def save(self): self.fetch(Server, 'server') - for username in self.subwidgets.keys(): - self.subwidgets[username].save() + for username, userdata in self.subwidgets.items(): + userdata.save() self.destruct() def defaultPort(self): @@ -1127,7 +1189,8 @@ class ServerEdit(Frame, MyWidget): # a custom port number you should be in expert mode and playing # close enough attention to notice this... self.service.set(defaultports[proto]) - if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED + if not proto in ("POP3", "APOP", "KPOP"): + self.uidl.state = DISABLED def user_edit(self, username, mode): self.subwidgets[username] = UserEdit(username, self).edit(mode, Toplevel()) @@ -1140,23 +1203,23 @@ class ServerEdit(Frame, MyWidget): def makeWidgets(self, host, mode): topwin = dispose_window(self, "Server options for querying " + host, serverhelp) - leftwin = Frame(self); - leftwidth = '25'; + leftwin = Frame(self) + leftwidth = '25' if mode != 'novice': ctlwin = Frame(leftwin, relief=RAISED, bd=5) Label(ctlwin, text="Run Controls").pack(side=TOP) Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP) Checkbutton(ctlwin, text='Pass messages with bad headers?', - variable=self.badheader).pack(side=TOP) + variable=self.badheader).pack(side=TOP) LabeledEntry(ctlwin, 'True name of ' + host + ':', - self.via, leftwidth).pack(side=TOP, fill=X) + self.via, leftwidth).pack(side=TOP, fill=X) LabeledEntry(ctlwin, 'Cycles to skip between polls:', - self.interval, leftwidth).pack(side=TOP, fill=X) + self.interval, leftwidth).pack(side=TOP, fill=X) LabeledEntry(ctlwin, 'Server timeout (seconds):', - self.timeout, leftwidth).pack(side=TOP, fill=X) + self.timeout, leftwidth).pack(side=TOP, fill=X) Button(ctlwin, text='Help', fg='blue', - command=lambda: helpwin(controlhelp)).pack(side=RIGHT) + command=lambda: helpwin(controlhelp)).pack(side=RIGHT) ctlwin.pack(fill=X) # Compute the available protocols from the compile-time options @@ -1181,11 +1244,11 @@ class ServerEdit(Frame, MyWidget): self.defaultPort) if mode != 'novice': LabeledEntry(protwin, 'On server TCP/IP service:', - self.service, leftwidth).pack(side=TOP, fill=X) + self.service, leftwidth).pack(side=TOP, fill=X) self.defaultPort() Checkbutton(protwin, - text="POP3: track `seen' with client-side UIDLs?", - variable=self.uidl).pack(side=TOP) + text="POP3: track `seen' with client-side UIDLs?", + variable=self.uidl).pack(side=TOP) Button(protwin, text='Probe for supported protocols', fg='blue', command=self.autoprobe).pack(side=LEFT) Button(protwin, text='Help', fg='blue', @@ -1195,32 +1258,32 @@ class ServerEdit(Frame, MyWidget): userwin = Frame(leftwin, relief=RAISED, bd=5) Label(userwin, text="User entries for " + host).pack(side=TOP) ListEdit("New user: ", - map(lambda x: x.remote, self.server.users), + [i.remote for i in self.server.users], lambda u, m=mode, s=self: s.user_edit(u, m), lambda u, s=self: s.user_delete(u), userwin, suserhelp) userwin.pack(fill=X) - leftwin.pack(side=LEFT, anchor=N, fill=X); + leftwin.pack(side=LEFT, anchor=N, fill=X) if mode != 'novice': - rightwin = Frame(self); + rightwin = Frame(self) mdropwin = Frame(rightwin, relief=RAISED, bd=5) Label(mdropwin, text="Multidrop options").pack(side=TOP) LabeledEntry(mdropwin, 'Envelope address header:', - self.envelope, '22').pack(side=TOP, fill=X) + self.envelope, '22').pack(side=TOP, fill=X) LabeledEntry(mdropwin, 'Envelope headers to skip:', - self.envskip, '22').pack(side=TOP, fill=X) + self.envskip, '22').pack(side=TOP, fill=X) LabeledEntry(mdropwin, 'Name prefix to strip:', - self.qvirtual, '22').pack(side=TOP, fill=X) + self.qvirtual, '22').pack(side=TOP, fill=X) Checkbutton(mdropwin, text="Enable multidrop DNS lookup?", - variable=self.dns).pack(side=TOP) + variable=self.dns).pack(side=TOP) Label(mdropwin, text="DNS aliases").pack(side=TOP) ListEdit("New alias: ", self.server.aka, None, None, mdropwin, None) Label(mdropwin, text="Domains to be considered local").pack(side=TOP) ListEdit("New domain: ", - self.server.localdomains, None, None, mdropwin, multihelp) + self.server.localdomains, None, None, mdropwin, multihelp) mdropwin.pack(fill=X) if os_type in ('linux', 'freebsd'): @@ -1228,17 +1291,17 @@ class ServerEdit(Frame, MyWidget): Label(secwin, text="Security").pack(side=TOP) # Don't actually let users set this. KPOP sets it implicitly ButtonBar(secwin, 'Authorization mode:', - self.auth, authlist, 2, None).pack(side=TOP) + self.auth, authlist, 2, None).pack(side=TOP) if os_type == 'linux' or os_type == 'freebsd' or 'interface' in dictmembers: LabeledEntry(secwin, 'IP range to check before poll:', - self.interface, leftwidth).pack(side=TOP, fill=X) + self.interface, leftwidth).pack(side=TOP, fill=X) if os_type == 'linux' or os_type == 'freebsd' or 'monitor' in dictmembers: LabeledEntry(secwin, 'Interface to monitor:', - self.monitor, leftwidth).pack(side=TOP, fill=X) + self.monitor, leftwidth).pack(side=TOP, fill=X) # Someday this should handle Kerberos 5 too if 'kerberos' in feature_options: LabeledEntry(secwin, 'Principal:', - self.principal, '12').pack(side=TOP, fill=X) + self.principal, '12').pack(side=TOP, fill=X) # ESMTP authentication LabeledEntry(secwin, 'ESMTP name:', self.esmtpname, '12').pack(side=TOP, fill=X) @@ -1248,7 +1311,7 @@ class ServerEdit(Frame, MyWidget): command=lambda: helpwin(sechelp)).pack(side=RIGHT) secwin.pack(fill=X) - rightwin.pack(side=LEFT, anchor=N); + rightwin.pack(side=LEFT, anchor=N) def autoprobe(self): # Note: this only handles case (1) near fetchmail.c:1032 @@ -1258,20 +1321,27 @@ class ServerEdit(Frame, MyWidget): realhost = self.server.via else: realhost = self.server.pollname - greetline = None - for protocol in ("IMAP","POP3","POP2"): - service = defaultports[protocol] - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect((realhost, ianaservices[service])) - greetline = sock.recv(1024) - sock.close() - except: - pass - else: + errors=[] + sslmode, protocol = None, None # will be used after loop + for sslmode in True, False: + for protocol in "IMAP", "POP3", "POP2": + service = defaultports[protocol] + if sslmode: + if service not in sslservices: + continue + port = sslservices[service] + else: + port = ianaservices[service] + greetline, address, new_errors = get_greetline(realhost, port, sslmode) + if new_errors: + errors += new_errors + if greetline: + break + if greetline: break + confwin = Toplevel() - if greetline == None: + if greetline is None: title = "Autoprobe of " + realhost + " failed" confirm = """ Fetchmailconf didn't find any mailservers active. @@ -1279,7 +1349,8 @@ This could mean the host doesn't support any, or that your Internet connection is down, or that the host is so slow that the probe timed out before getting a response. -""" + +""" + str('\n').join(errors) else: warnings = '' # OK, now try to recognize potential problems @@ -1298,7 +1369,7 @@ switch --enable-POP2. ### POP3 servers start here - if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.004") > 0: + if '1.003' in greetline or '1.004' in greetline: warnings = warnings + """ This appears to be an old version of the UC Davis POP server. These are dangerously unreliable (among other problems, they may drop your mailbox @@ -1308,7 +1379,7 @@ It is strongly recommended that you find a better POP3 server. The fetchmail FAQ includes pointers to good ones. """ - if string.find(greetline, "comcast.net") > 0: + if 'comcast.net' in greetline: warnings = warnings + """ The Comcast Maillennium POP3 server only returns the first 80K of a long message retrieved with TOP. Its response to RETR is normal, so use the @@ -1333,7 +1404,7 @@ message retrieved with TOP. Its response to RETR is normal, so use the # # +OK Cubic Circle's v1.31 1998/05/13 POP3 ready <6229000062f95036@wakko> # - if string.find(greetline, "Cubic Circle") > 0: + if 'Cubic Circle' in greetline: warnings = warnings + """ I see your server is running cucipop. Better make sure the server box isn't a SunOS 4.1.4 machine; cucipop tickles a bug in SunOS realloc() @@ -1344,7 +1415,7 @@ Also, some versions of cucipop don't assert an exclusive lock on your mailbox when it's being queried. This means that if you have more than one fetchmail query running against the same mailbox, bad things can happen. """ - if string.find(greetline, "David POP3 Server") > 0: + if 'David POP3 Server' in greetline: warnings = warnings + """ This POP3 server is badly broken. You should get rid of it -- and the brain-dead Microsoft operating system it rode in on. @@ -1353,19 +1424,19 @@ brain-dead Microsoft operating system it rode in on. # The greeting line on the server known to be buggy is: # +OK POP3 server ready (running FTGate V2, 2, 1, 0 Jun 21 1999 09:55:01) # - if string.find(greetline, "FTGate") > 0: + if 'FTGate' in greetline: warnings = warnings + """ This POP server has a weird bug; it says OK twice in response to TOP. Its response to RETR is normal, so use the `fetchall' option. """ - if string.find(greetline, " geonet.de") > 0: + if ' geonet.de' in greetline: warnings = warnings + """ You appear to be using geonet. As of late 2002, the TOP command on geonet's POP3 is broken. Use the fetchall option. """ - if string.find(greetline, "OpenMail") > 0: + if 'OpenMail' in greetline: warnings = warnings + """ You appear to be using some version of HP OpenMail. Many versions of OpenMail do not process the "TOP" command correctly; the symptom is that @@ -1374,7 +1445,7 @@ around this bug, turn on `fetchall' on all user entries associated with this server. """ - if string.find(greetline, "Escape character is") > 0: + if 'Escape character is' in greetline: warnings = warnings + """ Your greeting line looks like it was written by a fetid pile of camel dung identified to me as `popa3d written by Solar Designer'. @@ -1382,7 +1453,7 @@ Beware! The UIDL support in this thing is known to be completely broken, and other things probably are too. """ - if string.find(greetline, "MercuryP/NLM v1.48") > 0: + if 'MercuryP/NLM v1.48' in greetline: warnings = warnings + """ This is not a POP3 server. It has delusions of being one, but after RETR all messages are automatically marked to be deleted. The only @@ -1391,7 +1462,7 @@ Fetchmail does this, but we suspect this is probably broken in lots of other ways, too. """ - if string.find(greetline, "POP-Max") > 0: + if 'POP-Max' in greetline: warnings = warnings + """ The Mail Max POP3 server screws up on mail with attachments. It reports the message size with attachments included, but doesn't @@ -1400,13 +1471,13 @@ doesn't implement TOP correctly. You should get rid of it -- and the brain-dead NT server it rode in on. """ - if string.find(greetline, "POP3 Server Ready") > 0: + if 'POP3 Server Ready' in greetline: warnings = warnings + """ Some server that uses this greeting line has been observed to choke on TOP %d 99999999. Use the fetchall option. if necessary, to force RETR. """ - if string.find(greetline, "QPOP") > 0: + if 'QPOP' in greetline: warnings = warnings + """ This appears to be a version of Eudora qpopper. That's good. Fetchmail knows all about qpopper. However, be aware that the 2.53 version of @@ -1416,7 +1487,7 @@ it has been observed with fetchpop. The fix is to upgrade to qpopper 3.0beta or a more recent version. Better yet, switch to IMAP. """ - if string.find(greetline, " sprynet.com") > 0: + if ' sprynet.com' in greetline: warnings = warnings + """ You appear to be using a SpryNet server. In mid-1999 it was reported that the SpryNet TOP command marks messages seen. Therefore, for proper error @@ -1424,7 +1495,7 @@ recovery in the event of a line drop, it is strongly recommended that you turn on `fetchall' on all user entries associated with this server. """ - if string.find(greetline, "TEMS POP3") > 0: + if 'TEMS POP3' in greetline: warnings = warnings + """ Your POP3 server has "TEMS" in its header line. At least one such server does not process the "TOP" command correctly; the symptom is @@ -1433,7 +1504,7 @@ this bug, turn on `fetchall' on all user entries associated with this server. """ - if string.find(greetline, " spray.se") > 0: + if ' spray.se' in greetline: warnings = warnings + """ Your POP3 server has "spray.se" in its header line. In May 2000 at least one such server did not process the "TOP" command correctly; the @@ -1442,7 +1513,7 @@ this bug, turn on `fetchall' on all user entries associated with this server. """ - if string.find(greetline, " usa.net") > 0: + if ' usa.net' in greetline: warnings = warnings + """ You appear to be using USA.NET's free mail service. Their POP3 servers (at least as of the 2.2 version in use mid-1998) are quite flaky, but @@ -1456,14 +1527,14 @@ Therefore, it is strongly recommended that you turn on `fetchall' on all user entries associated with this server. """ - if string.find(greetline, " Novonyx POP3") > 0: + if ' Novonyx POP3' in greetline: warnings = warnings + """ Your mailserver is running Novonyx POP3. This server, at least as of version 2.17, seems to have problems handling and reporting seen bits. You may have to use the fetchall option. """ - if string.find(greetline, " IMS POP3") > 0: + if ' IMS POP3' in greetline: warnings = warnings + """ Some servers issuing the greeting line 'IMS POP3' have been known to do byte-stuffing incorrectly. This means that if a message you receive @@ -1474,7 +1545,7 @@ probably wedge itself. (This bug was recorded on IMS POP3 0.86.) ### IMAP servers start here - if string.find(greetline, "GroupWise") > 0: + if 'GroupWise' in greetline: warnings = warnings + """ The Novell GroupWise IMAP server would be better named GroupFoolish; it is (according to the designer of IMAP) unusably broken. Among @@ -1487,7 +1558,7 @@ with code as shoddy as GroupWise seems to be, you will probably pay for it with other problems.<p> """ - if string.find(greetline, "InterChange") > 0: + if 'InterChange' in greetline: warnings = warnings + """ The InterChange IMAP server at release levels below 3.61.08 screws up @@ -1499,7 +1570,7 @@ Exchange (quite legally under RFC2062) rejectsit. The InterChange folks claim to have fixed this bug in 3.61.08. """ - if string.find(greetline, "Imail") > 0: + if 'Imail' in greetline: warnings = warnings + """ We've seen a bug report indicating that this IMAP server (at least as of version 5.0.7) returns an invalid body size for messages with MIME @@ -1507,7 +1578,7 @@ attachments; the effect is to drop the attachments on the floor. We recommend you upgrade to a non-broken IMAP server. """ - if string.find(greetline, "Domino IMAP4") > 0: + if 'Domino IMAP4' in greetline: warnings = warnings + """ Your IMAP server appears to be Lotus Domino. This server, at least up to version 4.6.2a, has a bug in its generation of MIME boundaries (see @@ -1519,15 +1590,15 @@ POP3 facility is enabled, we recommend you fall back on it. ### Checks for protocol variants start here - closebrak = string.find(greetline, ">") - if closebrak > 0 and greetline[closebrak+1] == "\r": + closebrak = greetline.find(">") + if closebrak > 0 and greetline[closebrak+1] == "\r": warnings = warnings + """ It looks like you could use APOP on this server and avoid sending it your password in clear. You should talk to the mailserver administrator about this. """ - if string.find(greetline, "IMAP2bis") > 0: + if 'IMAP2bis' in greetline: warnings = warnings + """ IMAP2bis servers have a minor problem; they can't peek at messages without marking them seen. If you take a line hit during the retrieval, the @@ -1541,7 +1612,7 @@ To fix this bug, upgrade to an IMAP4 server. The fetchmail FAQ includes a pointer to an open-source implementation. """ - if string.find(greetline, "IMAP4rev1") > 0: + if 'IMAP4rev1' in greetline: warnings = warnings + """ I see an IMAP4rev1 server. Excellent. This is (a) the best kind of remote-mail server, and (b) the one the fetchmail author uses. Fetchmail @@ -1556,15 +1627,18 @@ Fetchmail doesn't know anything special about this server type. # Display success window with warnings title = "Autoprobe of " + realhost + " succeeded" - confirm = "The " + protocol + " server said:\n\n" + greetline + warnings + confirm = ("The {} server at {} port {} (SSL {}) said:\n\n" + .format(protocol, address[0], address[1], sslmode) + + greetline + warnings) self.protocol.set(protocol) self.service.set(defaultports[protocol]) + self.server.ssldefault = sslmode confwin.title(title) confwin.iconname(title) Label(confwin, text=title).pack() Message(confwin, text=confirm, width=600).pack() Button(confwin, text='Done', - command=lambda x=confwin: x.destroy(), bd=2).pack() + command=lambda x=confwin: x.destroy(), bd=2).pack() # # User editing stuff @@ -1609,23 +1683,27 @@ of sending it to your local system. class UserEdit(Frame, MyWidget): def __init__(self, username, parent): + Frame.__init__(self) + MyWidget.__init__(self) self.parent = parent self.user = None + self.keepalive = [] # Use this to anchor the PhotoImage object for user in parent.server.users: if user.remote == username: self.user = user - if self.user == None: + if self.user is None: self.user = User() self.user.remote = username self.user.localnames = [username] + self.user.ssl = parent.server.ssldefault parent.server.users.append(self.user) def edit(self, mode, master=None): Frame.__init__(self, master) Pack.config(self) self.master.title('Fetchmail user ' + self.user.remote - + ' querying ' + self.parent.server.pollname); - self.master.iconname('Fetchmail user ' + self.user.remote); + + ' querying ' + self.parent.server.pollname) + self.master.iconname('Fetchmail user ' + self.user.remote) self.post(User, 'user') self.makeWidgets(mode, self.parent.server.pollname) self.keepalive = [] # Use this to anchor the PhotoImage object @@ -1647,33 +1725,34 @@ class UserEdit(Frame, MyWidget): def save(self): ok = 0 - for x in self.user.localnames: ok = ok + (string.find(x, '@') != -1) - if ok == 0 or Dialog(self, - title = "Really accept an embedded '@' ?", - text = "Local names with an embedded '@', such as in foo@bar " - "might result in your mail being sent to foo@bar.com " - "instead of your local system.\n Are you sure you want " - "a local user name with an '@' in it?", - bitmap = 'question', - strings = ('Yes', 'No'), - default = 1).num == 0: - self.fetch(User, 'user') - self.destruct() + for x in self.user.localnames: + ok = ok + ('@' in x) + if (ok == 0 or Dialog(self, + title = "Really accept an embedded '@' ?", + text = "Local names with an embedded '@', such as in foo@bar " + "might result in your mail being sent to foo@bar.com " + "instead of your local system.\n Are you sure you want " + "a local user name with an '@' in it?", + bitmap = 'question', + strings = ('Yes', 'No'), + default = 1).num == 0): + self.fetch(User, 'user') + self.destruct() def makeWidgets(self, mode, servername): dispose_window(self, - "User options for " + self.user.remote + " querying " + servername, - userhelp) + "User options for " + self.user.remote + " querying " + servername, + userhelp) if mode != 'novice': - leftwin = Frame(self); + leftwin = Frame(self) else: leftwin = self secwin = Frame(leftwin, relief=RAISED, bd=5) Label(secwin, text="Authentication").pack(side=TOP) LabeledEntry(secwin, 'Password:', - self.password, '12').pack(side=TOP, fill=X) + self.password, '12').pack(side=TOP, fill=X) secwin.pack(fill=X, anchor=N) if 'ssl' in feature_options or 'ssl' in dictmembers: @@ -1697,7 +1776,7 @@ class UserEdit(Frame, MyWidget): names = Frame(leftwin, relief=RAISED, bd=5) Label(names, text="Local names").pack(side=TOP) ListEdit("New name: ", - self.user.localnames, None, None, names, localhelp) + self.user.localnames, None, None, names, localhelp) names.pack(fill=X, anchor=N) if mode != 'novice': @@ -1710,21 +1789,21 @@ class UserEdit(Frame, MyWidget): ListEdit("Domains:", self.user.fetchdomains, None, None, targwin, None) LabeledEntry(targwin, 'Use domain on RCPT TO line:', - self.smtpaddress, '26').pack(side=TOP, fill=X) + self.smtpaddress, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'Set fixed RCPT TO address:', - self.smtpname, '26').pack(side=TOP, fill=X) + self.smtpname, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'Connection setup command:', - self.preconnect, '26').pack(side=TOP, fill=X) + self.preconnect, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'Connection wrapup command:', - self.postconnect, '26').pack(side=TOP, fill=X) - LabeledEntry(targwin, 'Local delivery agent:', - self.mda, '26').pack(side=TOP, fill=X) + self.postconnect, '26').pack(side=TOP, fill=X) + LabeledEntry(targwin, 'Local delivery agent (MDA):', + self.mda, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'BSMTP output file:', - self.bsmtp, '26').pack(side=TOP, fill=X) + self.bsmtp, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'Listener spam-block codes:', - self.antispam, '26').pack(side=TOP, fill=X) + self.antispam, '26').pack(side=TOP, fill=X) LabeledEntry(targwin, 'Pass-through properties:', - self.properties, '26').pack(side=TOP, fill=X) + self.properties, '26').pack(side=TOP, fill=X) Checkbutton(targwin, text="Use LMTP?", variable=self.lmtp).pack(side=TOP, fill=X) targwin.pack(fill=X, anchor=N) @@ -1743,46 +1822,46 @@ class UserEdit(Frame, MyWidget): variable=self.fetchall).pack(side=TOP, anchor=W) if mode != 'novice': Checkbutton(optwin, text="Flush seen messages before retrieval", - variable=self.flush).pack(side=TOP, anchor=W) + variable=self.flush).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Flush oversized messages before retrieval", - variable=self.limitflush).pack(side=TOP, anchor=W) + variable=self.limitflush).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply", - variable=self.rewrite).pack(side=TOP, anchor=W) + variable=self.rewrite).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Force CR/LF at end of each line", - variable=self.forcecr).pack(side=TOP, anchor=W) + variable=self.forcecr).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Strip CR from end of each line", - variable=self.stripcr).pack(side=TOP, anchor=W) + variable=self.stripcr).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT", - variable=self.pass8bits).pack(side=TOP, anchor=W) + variable=self.pass8bits).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Undo MIME armoring on header and body", - variable=self.mimedecode).pack(side=TOP, anchor=W) + variable=self.mimedecode).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Drop Status lines from forwarded messages", - variable=self.dropstatus).pack(side=TOP, anchor=W) + variable=self.dropstatus).pack(side=TOP, anchor=W) Checkbutton(optwin, text="Drop Delivered-To lines from forwarded messages", - variable=self.dropdelivered).pack(side=TOP, anchor=W) + variable=self.dropdelivered).pack(side=TOP, anchor=W) optwin.pack(fill=X) if mode != 'novice': limwin = Frame(rightwin, relief=RAISED, bd=5) Label(limwin, text="Resource Limits").pack(side=TOP) LabeledEntry(limwin, 'Message size limit:', - self.limit, '30').pack(side=TOP, fill=X) + self.limit, '30').pack(side=TOP, fill=X) LabeledEntry(limwin, 'Size warning interval:', - self.warnings, '30').pack(side=TOP, fill=X) + self.warnings, '30').pack(side=TOP, fill=X) LabeledEntry(limwin, 'Max messages to fetch per poll:', - self.fetchlimit, '30').pack(side=TOP, fill=X) + self.fetchlimit, '30').pack(side=TOP, fill=X) LabeledEntry(limwin, 'Max message sizes to fetch per transaction:', - self.fetchsizelimit, '30').pack(side=TOP, fill=X) + self.fetchsizelimit, '30').pack(side=TOP, fill=X) if self.parent.server.protocol not in ('ETRN', 'ODMR'): LabeledEntry(limwin, 'Use fast UIDL:', - self.fastuidl, '30').pack(side=TOP, fill=X) + self.fastuidl, '30').pack(side=TOP, fill=X) LabeledEntry(limwin, 'Max messages to forward per poll:', - self.batchlimit, '30').pack(side=TOP, fill=X) + self.batchlimit, '30').pack(side=TOP, fill=X) if self.parent.server.protocol not in ('ETRN', 'ODMR'): LabeledEntry(limwin, 'Interval between expunges:', self.expunge, '30').pack(side=TOP, fill=X) Checkbutton(limwin, text="Idle after each poll (IMAP only)", - variable=self.idle).pack(side=TOP, anchor=W) + variable=self.idle).pack(side=TOP, anchor=W) limwin.pack(fill=X) if self.parent.server.protocol == 'IMAP': @@ -1809,8 +1888,8 @@ class Configurator(Frame): self.outfile = outfile self.onexit = onexit self.parent = parent - self.master.title('fetchmail configurator'); - self.master.iconname('fetchmail configurator'); + self.master.title('fetchmail configurator') + self.master.iconname('fetchmail configurator') Pack.config(self) self.keepalive = [] # Use this to anchor the PhotoImage object make_icon_window(self, fetchmail_icon) @@ -1821,14 +1900,14 @@ with this, you can easily set up a single-drop connection to one remote mail server. """, width=600).pack(side=TOP) Button(self, text='Novice Configuration', - fg='blue', command=self.novice).pack() + fg='blue', command=self.novice).pack() Message(self, text=""" Use `Expert Configuration' for advanced fetchmail setup, including multiple-site or multidrop connections. """, width=600).pack(side=TOP) Button(self, text='Expert Configuration', - fg='blue', command=self.expert).pack() + fg='blue', command=self.expert).pack() Message(self, text=""" Or you can just select `Quit' to leave the configurator now and @@ -1855,12 +1934,12 @@ class RunWindow(Frame): def __init__(self, command, master, parent): Frame.__init__(self, master) self.master = master - self.master.title('fetchmail run window'); - self.master.iconname('fetchmail run window'); + self.master.title('fetchmail run window') + self.master.iconname('fetchmail run window') Pack.config(self) Label(self, - text="Running "+command, - bd=2).pack(side=TOP, pady=10) + text="Running "+command, + bd=2).pack(side=TOP, pady=10) self.keepalive = [] # Use this to anchor the PhotoImage object make_icon_window(self, fetchmail_icon) @@ -1890,7 +1969,7 @@ class RunWindow(Frame): break self.textwidget.insert(END, ch) self.textwidget.insert(END, "Done.") - self.textwidget.see(END); + self.textwidget.see(END) def leave(self): self.master.destroy() @@ -1901,12 +1980,12 @@ class MainWindow(Frame): def __init__(self, outfile, master=None): Frame.__init__(self, master) self.outfile = outfile - self.master.title('fetchmail launcher'); - self.master.iconname('fetchmail launcher'); + self.master.title('fetchmail launcher') + self.master.iconname('fetchmail launcher') Pack.config(self) Label(self, - text='Fetchmailconf ' + version, - bd=2).pack(side=TOP, pady=10) + text='Fetchmailconf ' + VERSION, + bd=2).pack(side=TOP, pady=10) self.keepalive = [] # Use this to anchor the PhotoImage object make_icon_window(self, fetchmail_icon) self.debug = 0 @@ -1922,7 +2001,7 @@ servers it should poll (the host name, your username there, whether to use POP or IMAP, and so forth). """, width=600).pack(side=TOP) self.configbutton = Button(self, text='Configure fetchmail', - fg='blue', command=self.configure) + fg='blue', command=self.configure) self.configbutton.pack() Message(self, text=""" @@ -1982,7 +2061,7 @@ def setdiff(list1, list2): def copy_instance(toclass, fromdict): # Initialize a class object of given type from a conformant dictionary. - for fld in fromdict.keys(): + for fld in list(fromdict.keys()): if not fld in dictmembers: dictmembers.append(fld) # The `optional' fields are the ones we can ignore for purposes of @@ -1991,10 +2070,11 @@ def copy_instance(toclass, fromdict): optional = ('interface', 'monitor', 'esmtpname', 'esmtppassword', 'ssl', 'sslkey', 'sslcert', 'sslproto', 'sslcertck', - 'sslcertpath', 'sslcommonname', 'sslfingerprint', 'showdots') - class_sig = setdiff(toclass.__dict__.keys(), optional) + 'sslcertpath', 'sslcommonname', 'sslfingerprint', 'showdots', + 'ssldefault') + class_sig = setdiff(list(toclass.__dict__.keys()), optional) class_sig.sort() - dict_keys = setdiff(fromdict.keys(), optional) + dict_keys = setdiff(list(fromdict.keys()), optional) dict_keys.sort() common = intersect(class_sig, dict_keys) if 'typemap' in class_sig: @@ -2011,7 +2091,7 @@ def copy_instance(toclass, fromdict): print("Not matched in dictionary keys: " + repr(diff)) sys.exit(1) else: - for x in fromdict.keys(): + for x in list(fromdict.keys()): setattr(toclass, x, fromdict[x]) # @@ -2084,15 +2164,14 @@ gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7 # # Process options - (options, arguments) = getopt.getopt(sys.argv[1:], "df:hV", ["help", - "version"]) - dump = rcfile = None; + options, arguments = getopt.getopt(sys.argv[1:], "df:hV", ["help", "version"]) + dump = rcfile = None for (switch, val) in options: - if (switch == '-d'): + if switch == '-d': dump = TRUE - elif (switch == '-f'): + elif switch == '-f': rcfile = val - elif (switch == '-h' or switch == '--help'): + elif switch == '-h' or switch == '--help': print(""" Usage: fetchmailconf {[-d] [-f fetchmailrc]|-h|--help|-V|--version} -d - dump configuration (for debugging) @@ -2101,11 +2180,11 @@ Usage: fetchmailconf {[-d] [-f fetchmailrc]|-h|--help|-V|--version} --version, -V - print fetchmailconf version and quit """) sys.exit(0) - elif (switch == '-V' or switch == '--version'): - print("fetchmailconf %s" % version) + elif switch == '-V' or switch == '--version': + print("fetchmailconf %s" % VERSION) print(""" Copyright (C) 1997 - 2003 Eric S. Raymond -Copyright (C) 2005, 2006, 2008, 2009 Matthias Andree +Copyright (C) 2005 - 2020 Matthias Andree fetchmailconf comes with ABSOLUTELY NO WARRANTY. This is free software, you are welcome to redistribute it under certain conditions. Please see the file COPYING in the source or documentation directory for details.""") @@ -2133,13 +2212,21 @@ COPYING in the source or documentation directory for details.""") if s != 0: print("`" + cmd + "' run failure, status " + repr(s)) raise SystemExit - except: + except Exception as e: print("Unknown error while running fetchmail --configdump") + print(repr(e)) os.remove(tmpfile) sys.exit(1) + os_type = '' + feature_options = () + fetchmailrc = {} + try: - execfile(tmpfile) + if PY3K: + exec(compile(open(tmpfile, "rb").read(), tmpfile, 'exec')) + else: + execfile(tmpfile) except Exception as e: print("Can't read configuration output of fetchmail --configdump.") print(repr(e)) @@ -2156,17 +2243,21 @@ COPYING in the source or documentation directory for details.""") dictmembers = [] Fetchmailrc = Configuration() copy_instance(Fetchmailrc, fetchmailrc) - Fetchmailrc.servers = []; + Fetchmailrc.servers = [] for server in fetchmailrc['servers']: Newsite = Server() copy_instance(Newsite, server) Fetchmailrc.servers.append(Newsite) - Newsite.users = []; + Newsite.users = [] for user in server['users']: Newuser = User() copy_instance(Newuser, user) Newsite.users.append(Newuser) + # This is a hack, if one user uses SSL, all should + if Newuser.ssl: + Newsite.ssldefault = True + # We may want to display the configuration and quit if dump: print("This is a dump of the configuration we read:\n" + repr(Fetchmailrc)) |