]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf.py
Merge commit 'refs/merge-requests/4' of gitorious.org:fetchmail/fetchmail into 4...
[~andy/fetchmail] / fetchmailconf.py
1 #!/usr/bin/env python
2 #
3 # A GUI configurator for generating fetchmail configuration files.
4 # by Eric S. Raymond, <esr@snark.thyrsus.com>,
5 # Matthias Andree <matthias.andree@gmx.de>
6 # Requires Python with Tkinter, and the following OS-dependent services:
7 #       posix, posixpath, socket
8 version = "1.58"
9
10 from Tkinter import *
11 from Dialog import *
12 import sys, time, os, string, socket, getopt, tempfile
13
14 #
15 # Define the data structures the GUIs will be tossing around
16 #
17 class Configuration:
18     def __init__(self):
19         self.poll_interval = 0          # Normally, run in foreground
20         self.logfile = None             # No logfile, initially
21         self.idfile = os.environ["HOME"] + "/.fetchids"  # Default idfile, initially
22         self.postmaster = None          # No last-resort address, initially
23         self.bouncemail = TRUE          # Bounce errors to users
24         self.spambounce = FALSE         # Bounce spam errors
25         self.softbounce = TRUE          # Treat permanent error as temporary
26         self.properties = None          # No exiguous properties
27         self.invisible = FALSE          # Suppress Received line & spoof?
28         self.syslog = FALSE             # Use syslogd for logging?
29         self.servers = []               # List of included sites
30         Configuration.typemap = (
31             ('poll_interval',   'Int'),
32             ('logfile',  'String'),
33             ('idfile',    'String'),
34             ('postmaster',      'String'),
35             ('bouncemail',      'Boolean'),
36             ('spambounce',      'Boolean'),
37             ('softbounce',      'Boolean'),
38             ('properties',      'String'),
39             ('syslog',    'Boolean'),
40             ('invisible',       'Boolean'))
41
42     def __repr__(self):
43         str = "";
44         if self.syslog != ConfigurationDefaults.syslog:
45            str = str + ("set syslog\n")
46         elif self.logfile:
47             str = str + ("set logfile \"%s\"\n" % (self.logfile,));
48         if self.idfile != ConfigurationDefaults.idfile:
49             str = str + ("set idfile \"%s\"\n" % (self.idfile,));
50         if self.postmaster != ConfigurationDefaults.postmaster:
51             str = str + ("set postmaster \"%s\"\n" % (self.postmaster,));
52         if self.bouncemail:
53             str = str + ("set bouncemail\n")
54         else:
55             str = str + ("set nobouncemail\n")
56         if self.spambounce:
57             str = str + ("set spambounce\n")
58         else:
59             str = str + ("set no spambounce\n")
60         if self.softbounce:
61             str = str + ("set softbounce\n")
62         else:
63             str = str + ("set no softbounce\n")
64         if self.properties != ConfigurationDefaults.properties:
65             str = str + ("set properties \"%s\"\n" % (self.properties,));
66         if self.poll_interval > 0:
67             str = str + "set daemon " + `self.poll_interval` + "\n"
68         if self.invisible:
69            str = str + ("set invisible\n")
70         for site in self.servers:
71             str = str + repr(site)
72         return str
73
74     def __delitem__(self, name):
75         for si in range(len(self.servers)):
76             if self.servers[si].pollname == name:
77                 del self.servers[si]
78                 break
79
80     def __str__(self):
81         return "[Configuration: " + repr(self) + "]"
82
83 class Server:
84     def __init__(self):
85         self.pollname = None            # Poll label
86         self.via = None                 # True name of host
87         self.active = TRUE              # Poll status
88         self.interval = 0               # Skip interval
89         self.protocol = 'auto'          # Default to auto protocol
90         self.service = None             # Service name to use
91         self.uidl = FALSE               # Don't use RFC1725 UIDLs by default
92         self.auth = 'any'               # Default to password authentication
93         self.timeout = 300              # 5-minute timeout
94         self.envelope = 'Received'      # Envelope-address header
95         self.envskip = 0                # Number of envelope headers to skip
96         self.qvirtual = None            # Name prefix to strip
97         self.aka = []                   # List of DNS aka names
98         self.dns = TRUE                 # Enable DNS lookup on multidrop
99         self.localdomains = []          # Domains to be considered local
100         self.interface = None           # IP address and range
101         self.monitor = None             # IP address and range
102         self.plugin = None              # Plugin command for going to server
103         self.plugout = None             # Plugin command for going to listener
104         self.principal = None           # Kerberos principal
105         self.esmtpname = None           # ESMTP 2554 name
106         self.esmtppassword = None       # ESMTP 2554 password
107         self.tracepolls = FALSE         # Add trace-poll info to headers
108         self.badheader = FALSE          # Pass messages with bad headers on?
109         self.retrieveerror = 'abort'    # Policy when message retrieval errors encountered
110         self.users = []                 # List of user entries for site
111         Server.typemap = (
112             ('pollname',  'String'),
113             ('via',       'String'),
114             ('active',    'Boolean'),
115             ('interval',  'Int'),
116             ('protocol',  'String'),
117             ('service',   'String'),
118             ('uidl',      'Boolean'),
119             ('auth',      'String'),
120             ('timeout',   'Int'),
121             ('envelope',  'String'),
122             ('envskip',   'Int'),
123             ('qvirtual',  'String'),
124             # leave aka out
125             ('dns',       'Boolean'),
126             # leave localdomains out
127             ('interface', 'String'),
128             ('monitor',   'String'),
129             ('plugin',    'String'),
130             ('plugout',   'String'),
131             ('esmtpname', 'String'),
132             ('esmtppassword', 'String'),
133             ('principal', 'String'),
134             ('tracepolls','Boolean'),
135             ('badheader', 'Boolean'),
136             ('retrieveerror', 'String'))
137
138     def dump(self, folded):
139         res = ""
140         if self.active:   res = res + "poll"
141         else:        res = res + "skip"
142         res = res + (" " + self.pollname)
143         if self.via:
144             res = res + (" via " + str(self.via) + "\n");
145         if self.protocol != ServerDefaults.protocol:
146             res = res + " with proto " + self.protocol
147         if self.service and self.protocol and self.service != defaultports[self.protocol] and defaultports[self.protocol] and self.service != ianaservices[defaultports[self.protocol]]:
148             res = res + " service " + self.service
149         if self.timeout != ServerDefaults.timeout:
150             res = res + " timeout " + `self.timeout`
151         if self.interval != ServerDefaults.interval:
152             res = res + " interval " + `self.interval`
153         if self.envelope != ServerDefaults.envelope or self.envskip != ServerDefaults.envskip:
154             if self.envskip:
155                 res = res + " envelope " + `self.envskip` + " " + self.envelope
156             else:
157                 res = res + " envelope " + self.envelope
158         if self.qvirtual:
159             res = res + (" qvirtual " + str(self.qvirtual) + "\n");
160         if self.auth != ServerDefaults.auth:
161             res = res + " auth " + self.auth
162         if self.dns != ServerDefaults.dns or self.uidl != ServerDefaults.uidl:
163             res = res + " and options"
164         if self.dns != ServerDefaults.dns:
165             res = res + flag2str(self.dns, 'dns')
166         if self.uidl != ServerDefaults.uidl:
167             res = res + flag2str(self.uidl, 'uidl')
168         if folded:      res = res + "\n    "
169         else:        res = res + " "
170
171         if self.aka:
172              res = res + "aka"
173              for x in self.aka:
174                 res = res + " " + x
175         if self.aka and self.localdomains: res = res + " "
176         if self.localdomains:
177              res = res + ("localdomains")
178              for x in self.localdomains:
179                 res = res + " " + x
180         if (self.aka or self.localdomains):
181             if folded:
182                 res = res + "\n    "
183             else:
184                 res = res + " "
185
186         if self.tracepolls:
187            res = res + "tracepolls\n"
188
189         if self.interface:
190             res = res + " interface " + str(self.interface)
191         if self.monitor:
192             res = res + " monitor " + str(self.monitor)
193         if self.plugin:
194             res = res + " plugin " + `self.plugin`
195         if self.plugout:
196             res = res + " plugout " + `self.plugout`
197         if self.principal:
198             res = res + " principal " + `self.principal`
199         if self.esmtpname:
200             res = res + " esmtpname " + `self.esmtpname`
201         if self.esmtppassword:
202             res = res + " esmtppassword " + `self.esmtppassword`
203         if self.interface or self.monitor or self.principal or self.plugin or self.plugout:
204             if folded:
205                 res = res + "\n    "
206
207         if self.badheader:
208                 res = res + "bad-header accept "
209         if self.retrieveerror == 'continue':
210                 res = res + "retrieve-error continue "
211         if self.retrieveerror == 'markseen':
212                 res = res + "retrieve-error markseen "
213         if self.badheader or self.retrieveerror != ServerDefaults.retrieveerror:
214             if folded:
215                 res = res + "\n"
216
217         if res[-1] == " ": res = res[0:-1]
218
219         for user in self.users:
220             res = res + repr(user)
221         res = res + "\n"
222         return res;
223
224     def __delitem__(self, name):
225         for ui in range(len(self.users)):
226             if self.users[ui].remote == name:
227                 del self.users[ui]
228                 break
229
230     def __repr__(self):
231         return self.dump(TRUE)
232
233     def __str__(self):
234         return "[Server: " + self.dump(FALSE) + "]"
235
236 class User:
237     def __init__(self):
238         if os.environ.has_key("USER"):
239             self.remote = os.environ["USER"]    # Remote username
240         elif os.environ.has_key("LOGNAME"):
241             self.remote = os.environ["LOGNAME"]
242         else:
243             print "Can't get your username!"
244             sys.exit(1)
245         self.localnames = [self.remote,]# Local names
246         self.password = None    # Password for mail account access
247         self.mailboxes = []     # Remote folders to retrieve from
248         self.smtphunt = []      # Hosts to forward to
249         self.fetchdomains = []  # Domains to fetch from
250         self.smtpaddress = None # Append this to MAIL FROM line
251         self.smtpname = None    # Use this for RCPT TO
252         self.preconnect = None  # Connection setup
253         self.postconnect = None # Connection wrapup
254         self.mda = None         # Mail Delivery Agent
255         self.bsmtp = None       # BSMTP output file
256         self.lmtp = FALSE       # Use LMTP rather than SMTP?
257         self.antispam = ""      # Listener's spam-block code
258         self.keep = FALSE       # Keep messages
259         self.flush = FALSE      # Flush messages
260         self.limitflush = FALSE # Flush oversized messages
261         self.fetchall = FALSE   # Fetch old messages
262         self.rewrite = TRUE     # Rewrite message headers
263         self.forcecr = FALSE    # Force LF -> CR/LF
264         self.stripcr = FALSE    # Strip CR
265         self.pass8bits = FALSE  # Force BODY=7BIT
266         self.mimedecode = FALSE # Undo MIME armoring
267         self.dropstatus = FALSE # Drop incoming Status lines
268         self.dropdelivered = FALSE     # Drop incoming Delivered-To lines
269         self.idle = FALSE              # IDLE after poll
270         self.limit = 0          # Message size limit
271         self.warnings = 3600    # Size warning interval (see tunable.h)
272         self.fetchlimit = 0     # Max messages fetched per batch
273         self.fetchsizelimit = 100       # Max message sizes fetched per transaction
274         self.fastuidl = 4       # Do fast uidl 3 out of 4 times
275         self.batchlimit = 0     # Max message forwarded per batch
276         self.expunge = 0        # Interval between expunges (IMAP)
277         self.ssl = 0            # Enable Seccure Socket Layer
278         self.sslkey = None      # SSL key filename
279         self.sslcert = None     # SSL certificate filename
280         self.sslproto = None    # Force SSL?
281         self.sslcertck = 0      # Enable strict SSL cert checking
282         self.sslcertpath = None # Path to trusted certificates
283         self.sslcommonname = None       # SSL CommonName to expect
284         self.sslfingerprint = None      # SSL key fingerprint to check
285         self.properties = None  # Extension properties
286         User.typemap = (
287             ('remote',      'String'),
288             # leave out mailboxes and localnames
289             ('password',    'String'),
290             # Leave out smtphunt, fetchdomains
291             ('smtpaddress', 'String'),
292             ('smtpname', 'String'),
293             ('preconnect',  'String'),
294             ('postconnect', 'String'),
295             ('mda',      'String'),
296             ('bsmtp',       'String'),
297             ('lmtp',    'Boolean'),
298             ('antispam',    'String'),
299             ('keep',    'Boolean'),
300             ('flush',       'Boolean'),
301             ('limitflush',  'Boolean'),
302             ('fetchall',    'Boolean'),
303             ('rewrite',     'Boolean'),
304             ('forcecr',     'Boolean'),
305             ('stripcr',     'Boolean'),
306             ('pass8bits',   'Boolean'),
307             ('mimedecode',  'Boolean'),
308             ('dropstatus',  'Boolean'),
309             ('dropdelivered', 'Boolean'),
310             ('idle',    'Boolean'),
311             ('limit',       'Int'),
312             ('warnings',    'Int'),
313             ('fetchlimit',  'Int'),
314             ('fetchsizelimit',  'Int'),
315             ('fastuidl',    'Int'),
316             ('batchlimit',  'Int'),
317             ('expunge',     'Int'),
318             ('ssl',      'Boolean'),
319             ('sslkey',      'String'),
320             ('sslcert',     'String'),
321             ('sslcertck',   'Boolean'),
322             ('sslcertpath', 'String'),
323             ('sslcommonname', 'String'),
324             ('sslfingerprint', 'String'),
325             ('properties',  'String'))
326
327     def __repr__(self):
328         res = "    "
329         res = res + "user " + `self.remote` + " there ";
330         if self.password:
331             res = res + "with password " + `self.password` + " "
332         if self.localnames:
333             res = res + "is"
334             for x in self.localnames:
335                 res = res + " " + `x`
336             res = res + " here"
337         if (self.keep != UserDefaults.keep
338                 or self.flush != UserDefaults.flush
339                 or self.limitflush != UserDefaults.limitflush
340                 or self.fetchall != UserDefaults.fetchall
341                 or self.rewrite != UserDefaults.rewrite
342                 or self.forcecr != UserDefaults.forcecr
343                 or self.stripcr != UserDefaults.stripcr
344                 or self.pass8bits != UserDefaults.pass8bits
345                 or self.mimedecode != UserDefaults.mimedecode
346                 or self.dropstatus != UserDefaults.dropstatus
347                 or self.dropdelivered != UserDefaults.dropdelivered
348                 or self.idle != UserDefaults.idle):
349             res = res + " options"
350         if self.keep != UserDefaults.keep:
351             res = res + flag2str(self.keep, 'keep')
352         if self.flush != UserDefaults.flush:
353             res = res + flag2str(self.flush, 'flush')
354         if self.limitflush != UserDefaults.limitflush:
355             res = res + flag2str(self.limitflush, 'limitflush')
356         if self.fetchall != UserDefaults.fetchall:
357             res = res + flag2str(self.fetchall, 'fetchall')
358         if self.rewrite != UserDefaults.rewrite:
359             res = res + flag2str(self.rewrite, 'rewrite')
360         if self.forcecr != UserDefaults.forcecr:
361             res = res + flag2str(self.forcecr, 'forcecr')
362         if self.stripcr != UserDefaults.stripcr:
363             res = res + flag2str(self.stripcr, 'stripcr')
364         if self.pass8bits != UserDefaults.pass8bits:
365             res = res + flag2str(self.pass8bits, 'pass8bits')
366         if self.mimedecode != UserDefaults.mimedecode:
367             res = res + flag2str(self.mimedecode, 'mimedecode')
368         if self.dropstatus != UserDefaults.dropstatus:
369             res = res + flag2str(self.dropstatus, 'dropstatus')
370         if self.dropdelivered != UserDefaults.dropdelivered:
371             res = res + flag2str(self.dropdelivered, 'dropdelivered')
372         if self.idle != UserDefaults.idle:
373             res = res + flag2str(self.idle, 'idle')
374         if self.limit != UserDefaults.limit:
375             res = res + " limit " + `self.limit`
376         if self.warnings != UserDefaults.warnings:
377             res = res + " warnings " + `self.warnings`
378         if self.fetchlimit != UserDefaults.fetchlimit:
379             res = res + " fetchlimit " + `self.fetchlimit`
380         if self.fetchsizelimit != UserDefaults.fetchsizelimit:
381             res = res + " fetchsizelimit " + `self.fetchsizelimit`
382         if self.fastuidl != UserDefaults.fastuidl:
383             res = res + " fastuidl " + `self.fastuidl`
384         if self.batchlimit != UserDefaults.batchlimit:
385             res = res + " batchlimit " + `self.batchlimit`
386         if self.ssl and self.ssl != UserDefaults.ssl:
387             res = res + flag2str(self.ssl, 'ssl')
388         if self.sslkey and self.sslkey != UserDefaults.sslkey:
389             res = res + " sslkey " + `self.sslkey`
390         if self.sslcert and self.sslcert != UserDefaults.sslcert:
391             res = res + " sslcert " + `self.sslcert`
392         if self.sslproto and self.sslproto != UserDefaults.sslproto:
393             res = res + " sslproto " + `self.sslproto`
394         if self.sslcertck and self.sslcertck != UserDefaults.sslcertck:
395             res = res +  flag2str(self.sslcertck, 'sslcertck')
396         if self.sslcertpath and self.sslcertpath != UserDefaults.sslcertpath:
397             res = res + " sslcertpath " + `self.sslcertpath`
398         if self.sslcommonname and self.sslcommonname != UserDefaults.sslcommonname:
399             res = res + " sslcommonname " + `self.sslcommonname`
400         if self.sslfingerprint and self.sslfingerprint != UserDefaults.sslfingerprint:
401             res = res + " sslfingerprint " + `self.sslfingerprint`
402         if self.expunge != UserDefaults.expunge:
403             res = res + " expunge " + `self.expunge`
404         res = res + "\n"
405         trimmed = self.smtphunt;
406         if trimmed != [] and trimmed[len(trimmed) - 1] == "localhost":
407             trimmed = trimmed[0:len(trimmed) - 1]
408         if trimmed != [] and trimmed[len(trimmed) - 1] == hostname:
409             trimmed = trimmed[0:len(trimmed) - 1]
410         if trimmed != []:
411             res = res + "    smtphost "
412             for x in trimmed:
413                 res = res + " " + x
414                 res = res + "\n"
415         trimmed = self.fetchdomains
416         if trimmed != [] and trimmed[len(trimmed) - 1] == hostname:
417             trimmed = trimmed[0:len(trimmed) - 1]
418         if trimmed != []:
419             res = res + "    fetchdomains "
420             for x in trimmed:
421                 res = res + " " + x
422                 res = res + "\n"
423         if self.mailboxes:
424              res = res + "    folder"
425              for x in self.mailboxes:
426                 res = res + ' "%s"' % x
427              res = res + "\n"
428         for fld in ('smtpaddress', 'preconnect', 'postconnect', 'mda', 'bsmtp', 'properties'):
429             if getattr(self, fld):
430                 res = res + " %s %s\n" % (fld, `getattr(self, fld)`)
431         if self.lmtp != UserDefaults.lmtp:
432             res = res + flag2str(self.lmtp, 'lmtp')
433         if self.antispam != UserDefaults.antispam:
434             res = res + "    antispam " + self.antispam + "\n"
435         return res;
436
437     def __str__(self):
438         return "[User: " + repr(self) + "]"
439
440 #
441 # Helper code
442 #
443
444 # IANA port assignments and bogus 1109 entry
445 ianaservices = {"pop3":110,
446                 "1109":1109,
447                 "imap":143,
448                 "smtp":25,
449                 "odmr":366}
450
451 # fetchmail protocol to IANA service name
452 defaultports = {"auto":None,
453                 "POP3":"pop3",
454                 "APOP":"pop3",
455                 "KPOP":"1109",
456                 "IMAP":"imap",
457                 "ETRN":"smtp",
458                 "ODMR":"odmr"}
459
460 authlist = ("any", "password", "gssapi", "kerberos", "ssh", "otp",
461             "msn", "ntlm")
462
463 listboxhelp = {
464     'title' : 'List Selection Help',
465     'banner': 'List Selection',
466     'text' : """
467 You must select an item in the list box (by clicking on it).
468 """}
469
470 def flag2str(value, string):
471 # make a string representation of a .fetchmailrc flag or negated flag
472     str = ""
473     if value != None:
474         str = str + (" ")
475         if value == FALSE: str = str + ("no ")
476         str = str + string;
477     return str
478
479 class LabeledEntry(Frame):
480 # widget consisting of entry field with caption to left
481     def bind(self, key, action):
482         self.E.bind(key, action)
483     def focus_set(self):
484         self.E.focus_set()
485     def __init__(self, Master, text, textvar, lwidth, ewidth=12):
486         Frame.__init__(self, Master)
487         self.L = Label(self, {'text':text, 'width':lwidth, 'anchor':'w'})
488         self.E = Entry(self, {'textvar':textvar, 'width':ewidth})
489         self.L.pack({'side':'left'})
490         self.E.pack({'side':'left', 'expand':'1', 'fill':'x'})
491
492 def ButtonBar(frame, legend, ref, alternatives, depth, command):
493 # array of radio buttons, caption to left, picking from a string list
494     bar = Frame(frame)
495     width = (len(alternatives)+1) / depth;
496     Label(bar, text=legend).pack(side=LEFT)
497     for column in range(width):
498         subframe = Frame(bar)
499         for row in range(depth):
500             ind = width * row + column
501             if ind < len(alternatives):
502                 Radiobutton(subframe,
503                         {'text':alternatives[ind],
504                          'variable':ref,
505                          'value':alternatives[ind],
506                          'command':command}).pack(side=TOP, anchor=W)
507             else:
508                 # This is just a spacer
509                 Radiobutton(subframe,
510                         {'text':" ",'state':DISABLED}).pack(side=TOP, anchor=W)
511         subframe.pack(side=LEFT)
512     bar.pack(side=TOP);
513     return bar
514
515 def helpwin(helpdict):
516 # help message window with a self-destruct button
517     helpwin = Toplevel()
518     helpwin.title(helpdict['title'])
519     helpwin.iconname(helpdict['title'])
520     Label(helpwin, text=helpdict['banner']).pack()
521     textframe = Frame(helpwin)
522     scroll = Scrollbar(textframe)
523     helpwin.textwidget = Text(textframe, setgrid=TRUE)
524     textframe.pack(side=TOP, expand=YES, fill=BOTH)
525     helpwin.textwidget.config(yscrollcommand=scroll.set)
526     helpwin.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
527     scroll.config(command=helpwin.textwidget.yview)
528     scroll.pack(side=RIGHT, fill=BOTH)
529     helpwin.textwidget.insert(END, helpdict['text']);
530     Button(helpwin, text='Done',
531            command=lambda x=helpwin: x.destroy(), bd=2).pack()
532     textframe.pack(side=TOP)
533
534 def make_icon_window(base, image):
535     try:
536         # Some older pythons will error out on this
537         icon_image = PhotoImage(data=image)
538         icon_window = Toplevel()
539         Label(icon_window, image=icon_image, bg='black').pack()
540         base.master.iconwindow(icon_window)
541         # Avoid TkInter brain death. PhotoImage objects go out of
542         # scope when the enclosing function returns.  Therefore
543         # we have to explicitly link them to something.
544         base.keepalive.append(icon_image)
545     except:
546         pass
547
548 class ListEdit(Frame):
549 # edit a list of values (duplicates not allowed) with a supplied editor hook
550     def __init__(self, newlegend, list, editor, deletor, master, helptxt):
551         self.editor = editor
552         self.deletor = deletor
553         self.list = list
554
555         # Set up a widget to accept new elements
556         self.newval = StringVar(master)
557         newwin = LabeledEntry(master, newlegend, self.newval, '12')
558         newwin.bind('<Double-1>', self.handleNew)
559         newwin.bind('<Return>', self.handleNew)
560         newwin.pack(side=TOP, fill=X, anchor=E)
561
562         # Edit the existing list
563         listframe = Frame(master)
564         scroll = Scrollbar(listframe)
565         self.listwidget = Listbox(listframe, height=0, selectmode='browse')
566         if self.list:
567             for x in self.list:
568                 self.listwidget.insert(END, x)
569         listframe.pack(side=TOP, expand=YES, fill=BOTH)
570         self.listwidget.config(yscrollcommand=scroll.set)
571         self.listwidget.pack(side=LEFT, expand=YES, fill=BOTH)
572         scroll.config(command=self.listwidget.yview)
573         scroll.pack(side=RIGHT, fill=BOTH)
574         self.listwidget.config(selectmode=SINGLE, setgrid=TRUE)
575         self.listwidget.bind('<Double-1>', self.handleList);
576         self.listwidget.bind('<Return>', self.handleList);
577
578         bf = Frame(master);
579         if self.editor:
580             Button(bf, text='Edit',   command=self.editItem).pack(side=LEFT)
581         Button(bf, text='Delete', command=self.deleteItem).pack(side=LEFT)
582         if helptxt:
583             self.helptxt = helptxt
584             Button(bf, text='Help', fg='blue',
585                    command=self.help).pack(side=RIGHT)
586         bf.pack(fill=X)
587
588     def help(self):
589         helpwin(self.helptxt)
590
591     def handleList(self, event):
592         self.editItem();
593
594     def handleNew(self, event):
595         item = self.newval.get()
596         if item:
597             entire = self.listwidget.get(0, self.listwidget.index('end'));
598             if item and (not entire) or (not item in self.listwidget.get(0, self.listwidget.index('end'))):
599                 self.listwidget.insert('end', item)
600                 if self.list != None: self.list.append(item)
601                 if self.editor:
602                     apply(self.editor, (item,))
603             self.newval.set('')
604
605     def editItem(self):
606         select = self.listwidget.curselection()
607         if not select:
608             helpwin(listboxhelp)
609         else:
610             index = select[0]
611             if index and self.editor:
612                 label = self.listwidget.get(index);
613                 if self.editor:
614                     apply(self.editor, (label,))
615
616     def deleteItem(self):
617         select = self.listwidget.curselection()
618         if not select:
619             helpwin(listboxhelp)
620         else:
621             index = string.atoi(select[0])
622             label = self.listwidget.get(index);
623             self.listwidget.delete(index)
624             if self.list != None:
625                 del self.list[index]
626             if self.deletor != None:
627                 apply(self.deletor, (label,))
628
629 def ConfirmQuit(frame, context):
630     ans = Dialog(frame,
631                  title = 'Quit?',
632                  text = 'Really quit ' + context + ' without saving?',
633                  bitmap = 'question',
634                  strings = ('Yes', 'No'),
635                  default = 1)
636     return ans.num == 0
637
638 def dispose_window(master, legend, help, savelegend='OK'):
639     dispose = Frame(master, relief=RAISED, bd=5)
640     Label(dispose, text=legend).pack(side=TOP,pady=10)
641     Button(dispose, text=savelegend, fg='blue',
642            command=master.save).pack(side=LEFT)
643     Button(dispose, text='Quit', fg='blue',
644            command=master.nosave).pack(side=LEFT)
645     Button(dispose, text='Help', fg='blue',
646            command=lambda x=help: helpwin(x)).pack(side=RIGHT)
647     dispose.pack(fill=X)
648     return dispose
649
650 class MyWidget:
651 # Common methods for Tkinter widgets -- deals with Tkinter declaration
652     def post(self, widgetclass, field):
653         for x in widgetclass.typemap:
654             if x[1] == 'Boolean':
655                 setattr(self, x[0], BooleanVar(self))
656             elif x[1] == 'String':
657                 setattr(self, x[0], StringVar(self))
658             elif x[1] == 'Int':
659                 setattr(self, x[0], IntVar(self))
660             source = getattr(getattr(self, field), x[0])
661             if source:
662                 getattr(self, x[0]).set(source)
663
664     def fetch(self, widgetclass, field):
665         for x in widgetclass.typemap:
666             setattr(getattr(self, field), x[0], getattr(self, x[0]).get())
667
668 #
669 # First, code to set the global fetchmail run controls.
670 #
671
672 configure_novice_help = {
673     'title' : 'Fetchmail novice configurator help',
674     'banner': 'Novice configurator help',
675     'text' : """
676 In the `Novice Configurator Controls' panel, you can:
677
678 Press `Save' to save the new fetchmail configuration you have created.
679 Press `Quit' to exit without saving.
680 Press `Help' to bring up this help message.
681
682 In the `Novice Configuration' panels, you will set up the basic data
683 needed to create a simple fetchmail setup.  These include:
684
685 1. The name of the remote site you want to query.
686
687 2. Your login name on that site.
688
689 3. Your password on that site.
690
691 4. A protocol to use (POP, IMAP, ETRN, etc.)
692
693 5. A poll interval in seconds.
694    If 0, fetchmail will run in the foreground once when started.
695    If > 0, fetchmail will run in the background and start a new poll
696    cycle after the interval has elapsed.
697
698 6. Options to fetch old messages as well as new, or to suppress
699    deletion of fetched message.
700
701 The novice-configuration code will assume that you want to forward mail
702 to a local sendmail listener with no special options.
703 """}
704
705 configure_expert_help = {
706     'title' : 'Fetchmail expert configurator help',
707     'banner': 'Expert configurator help',
708     'text' : """
709 In the `Expert Configurator Controls' panel, you can:
710
711 Press `Save' to save the new fetchmail configuration you have edited.
712 Press `Quit' to exit without saving.
713 Press `Help' to bring up this help message.
714
715 In the `Run Controls' panel, you can set the following options that
716 control how fetchmail runs:
717
718 Poll interval
719         Number of seconds to wait between polls in the background.
720         If zero, fetchmail will run in foreground.
721
722 Logfile
723         If empty, emit progress and error messages to stderr.
724         Otherwise this gives the name of the files to write to.
725         This field is ignored if the "Log to syslog?" option is on.
726
727 Idfile
728         If empty, store seen-message IDs in .fetchids under user's home
729         directory.  If nonempty, use given file name.
730
731 Postmaster
732         Who to send multidrop mail to as a last resort if no address can
733         be matched.  Normally empty; in this case, fetchmail treats the
734         invoking user as the address of last resort unless that user is
735         root.  If that user is root, fetchmail sends to `postmaster'.
736
737 Bounces to sender?
738         If this option is on (the default) error mail goes to the sender.
739         Otherwise it goes to the postmaster.
740
741 Send spam bounces?
742         If this option is on, spam bounces are sent to the sender or
743         postmaster (depending on the "Bounces to sender?" option.  Otherwise,
744         spam bounces are not sent (the default).
745
746 Use soft bounces?
747         If this option is on, permanent delivery errors are treated as
748         temporary, i. e. mail is kept on the upstream server. Useful
749         during testing and after configuration changes, and on by
750         default.
751           If this option is off, permanent delivery errors delete
752         undeliverable mail from the upstream.
753
754 Invisible
755         If false (the default) fetchmail generates a Received line into
756         each message and generates a HELO from the machine it is running on.
757         If true, fetchmail generates no Received line and HELOs as if it were
758         the remote site.
759
760 In the `Remote Mail Configurations' panel, you can:
761
762 1. Enter the name of a new remote mail server you want fetchmail to query.
763
764 To do this, simply enter a label for the poll configuration in the
765 `New Server:' box.  The label should be a DNS name of the server (unless
766 you are using ssh or some other tunneling method and will fill in the `via'
767 option on the site configuration screen).
768
769 2. Change the configuration of an existing site.
770
771 To do this, find the site's label in the listbox and double-click it.
772 This will take you to a site configuration dialogue.
773 """}
774
775
776 class ConfigurationEdit(Frame, MyWidget):
777     def __init__(self, configuration, outfile, master, onexit):
778         self.subwidgets = {}
779         self.configuration = configuration
780         self.outfile = outfile
781         self.container = master
782         self.onexit = onexit
783         ConfigurationEdit.mode_to_help = {
784             'novice':configure_novice_help, 'expert':configure_expert_help
785             }
786
787     def server_edit(self, sitename):
788         self.subwidgets[sitename] = ServerEdit(sitename, self).edit(self.mode, Toplevel())
789
790     def server_delete(self, sitename):
791         try:
792             for user in self.subwidgets.keys():
793                 user.destruct()
794             del self.configuration[sitename]
795         except:
796             pass
797
798     def edit(self, mode):
799         self.mode = mode
800         Frame.__init__(self, self.container)
801         self.master.title('fetchmail ' + self.mode + ' configurator');
802         self.master.iconname('fetchmail ' + self.mode + ' configurator');
803         self.master.protocol('WM_DELETE_WINDOW', self.nosave)
804         self.keepalive = []     # Use this to anchor the PhotoImage object
805         make_icon_window(self, fetchmail_icon)
806         Pack.config(self)
807         self.post(Configuration, 'configuration')
808
809         dispose_window(self,
810                        'Configurator ' + self.mode + ' Controls',
811                        ConfigurationEdit.mode_to_help[self.mode],
812                        'Save')
813
814         gf = Frame(self, relief=RAISED, bd = 5)
815         Label(gf,
816                 text='Fetchmail Run Controls',
817                 bd=2).pack(side=TOP, pady=10)
818
819         df = Frame(gf)
820
821         ff = Frame(df)
822         if self.mode != 'novice':
823             # Set the postmaster
824             log = LabeledEntry(ff, '     Postmaster:', self.postmaster, '14')
825             log.pack(side=RIGHT, anchor=E)
826
827         # Set the poll interval
828         de = LabeledEntry(ff, '     Poll interval:', self.poll_interval, '14')
829         de.pack(side=RIGHT, anchor=E)
830         ff.pack()
831
832         df.pack()
833
834         if self.mode != 'novice':
835             pf = Frame(gf)
836             Checkbutton(pf,
837                 {'text':'Bounces to sender?',
838                 'variable':self.bouncemail,
839                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
840             pf.pack(fill=X)
841
842             sb = Frame(gf)
843             Checkbutton(sb,
844                 {'text':'Send spam bounces?',
845                 'variable':self.spambounce,
846                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
847             sb.pack(fill=X)
848
849             sb = Frame(gf)
850             Checkbutton(sb,
851                 {'text':'Treat permanent errors as temporary?',
852                 'variable':self.softbounce,
853                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
854             sb.pack(fill=X)
855
856             sf = Frame(gf)
857             Checkbutton(sf,
858                 {'text':'Log to syslog?',
859                 'variable':self.syslog,
860                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
861             log = LabeledEntry(sf, '     Logfile:', self.logfile, '14')
862             log.pack(side=RIGHT, anchor=E)
863             sf.pack(fill=X)
864
865             Checkbutton(gf,
866                 {'text':'Invisible mode?',
867                 'variable':self.invisible,
868                  'relief':GROOVE}).pack(side=LEFT, anchor=W)
869             # Set the idfile
870             log = LabeledEntry(gf, '     Idfile:', self.idfile, '14')
871             log.pack(side=RIGHT, anchor=E)
872
873         gf.pack(fill=X)
874
875         # Expert mode allows us to edit multiple sites
876         lf = Frame(self, relief=RAISED, bd=5)
877         Label(lf,
878               text='Remote Mail Server Configurations',
879               bd=2).pack(side=TOP, pady=10)
880         ListEdit('New Server:',
881                 map(lambda x: x.pollname, self.configuration.servers),
882                 lambda site, self=self: self.server_edit(site),
883                 lambda site, self=self: self.server_delete(site),
884                 lf, remotehelp)
885         lf.pack(fill=X)
886
887     def destruct(self):
888         for sitename in self.subwidgets.keys():
889             self.subwidgets[sitename].destruct()
890         self.master.destroy()
891         self.onexit()
892
893     def nosave(self):
894         if ConfirmQuit(self, self.mode + " configuration editor"):
895             self.destruct()
896
897     def save(self):
898         for sitename in self.subwidgets.keys():
899             self.subwidgets[sitename].save()
900         self.fetch(Configuration, 'configuration')
901         fm = None
902         if not self.outfile:
903             fm = sys.stdout
904         elif not os.path.isfile(self.outfile) or Dialog(self,
905                  title = 'Overwrite existing run control file?',
906                  text = 'Really overwrite existing run control file?',
907                  bitmap = 'question',
908                  strings = ('Yes', 'No'),
909                  default = 1).num == 0:
910             try:
911                 os.rename(self.outfile, self.outfile + "~")
912             # Pre-1.5.2 compatibility...
913             except os.error:
914                 pass
915             oldumask = os.umask(077)
916             fm = open(self.outfile, 'w')
917             os.umask(oldumask)
918         if fm:
919             # be paranoid
920             if fm != sys.stdout:
921                 os.chmod(self.outfile, 0600)
922             fm.write("# Configuration created %s by fetchmailconf %s\n" % (time.ctime(time.time()), version))
923             fm.write(`self.configuration`)
924             if self.outfile:
925                 fm.close()
926             self.destruct()
927
928 #
929 # Server editing stuff.
930 #
931 remotehelp = {
932     'title' : 'Remote site help',
933     'banner': 'Remote sites',
934     'text' : """
935 When you add a site name to the list here,
936 you initialize an entry telling fetchmail
937 how to poll a new site.
938
939 When you select a sitename (by double-
940 clicking it, or by single-clicking to
941 select and then clicking the Edit button),
942 you will open a window to configure that
943 site.
944 """}
945
946 serverhelp = {
947     'title' : 'Server options help',
948     'banner': 'Server Options',
949     'text' : """
950 The server options screen controls fetchmail
951 options that apply to one of your mailservers.
952
953 Once you have a mailserver configuration set
954 up as you like it, you can select `OK' to
955 store it in the server list maintained in
956 the main configuration window.
957
958 If you wish to discard changes to a server
959 configuration, select `Quit'.
960 """}
961
962 controlhelp = {
963     'title' : 'Run Control help',
964     'banner': 'Run Controls',
965     'text' : """
966 If the `Poll normally' checkbox is on, the host is polled as part of
967 the normal operation of fetchmail when it is run with no arguments.
968 If it is off, fetchmail will only query this host when it is given as
969 a command-line argument.
970
971 The `Retrieve Error Policy' specifies how server errors during
972 message retrieval are handled.  The default behaviour is to abort the
973 current session.  Both the continue and markseen options will skip
974 the message with the error, but continue the session allowing for 
975 downloading of subsequent messages.  Additionally, the markseen
976 option will mark the skipped message as seen.
977  
978 The `True name of server' box should specify the actual DNS name
979 to query. By default this is the same as the poll name.
980
981 Normally each host described in the file is queried once each
982 poll cycle. If `Cycles to skip between polls' is greater than 0,
983 that's the number of poll cycles that are skipped between the
984 times this post is actually polled.
985
986 The `Server timeout' is the number of seconds fetchmail will wait
987 for a reply from the mailserver before concluding it is hung and
988 giving up.
989 """}
990
991 protohelp = {
992     'title' : 'Protocol and Port help',
993     'banner': 'Protocol and Port',
994     'text' : """
995 These options control the remote-mail protocol
996 and TCP/IP service port used to query this
997 server.
998
999 If you click the `Probe for supported protocols'
1000 button, fetchmail will try to find you the most
1001 capable server on the selected host (this will
1002 only work if you're conncted to the Internet).
1003 The probe only checks for ordinary IMAP and POP
1004 protocols; fortunately these are the most
1005 frequently supported.
1006
1007 The `Protocol' button bar offers you a choice of
1008 all the different protocols available.  The `auto'
1009 protocol is the default mode; it probes the host
1010 ports for POP3 and IMAP to see if either is
1011 available.
1012
1013 Normally the TCP/IP service port to use is
1014 dictated by the protocol choice.  The `Service'
1015 field (only present in expert mode) lets you
1016 set a non-standard service (port).
1017 """}
1018
1019 sechelp = {
1020     'title' : 'Security option help',
1021     'banner': 'Security',
1022     'text' : """
1023 The 'authorization mode' allows you to choose the
1024 mode that fetchmail uses to log in to your server. You
1025 can usually leave this at 'any', but you will have to pick
1026 'NTLM' and 'MSN' manually for the nonce.
1027
1028 The 'interface' option allows you to specify a range
1029 of IP addresses to monitor for activity.  If these
1030 addresses are not active, fetchmail will not poll.
1031 Specifying this may protect you from a spoofing attack
1032 if your client machine has more than one IP gateway
1033 address and some of the gateways are to insecure nets.
1034
1035 The `monitor' option, if given, specifies the only
1036 device through which fetchmail is permitted to connect
1037 to servers.  This option may be used to prevent
1038 fetchmail from triggering an expensive dial-out if the
1039 interface is not already active.
1040
1041 The `interface' and `monitor' options are available
1042 only for Linux and freeBSD systems.  See the fetchmail
1043 manual page for details on these.
1044
1045 The ssl option enables SSL communication with a mailserver
1046 supporting Secure Sockets Layer. The sslkey and sslcert options
1047 declare key and certificate files for use with SSL.
1048 The sslcertck option enables strict checking of SSL server
1049 certificates (and sslcertpath gives the trusted certificate
1050 directory). The sslcommonname option helps if the server is
1051 misconfigured and returning "Server CommonName mismatch"
1052 warnings. With sslfingerprint, you can specify a finger-
1053 print the server's key is checked against.
1054 """}
1055
1056 multihelp = {
1057     'title' : 'Multidrop option help',
1058     'banner': 'Multidrop',
1059     'text' : """
1060 These options are only useful with multidrop mode.
1061 See the manual page for extended discussion.
1062 """}
1063
1064 suserhelp = {
1065     'title' : 'User list help',
1066     'banner': 'User list',
1067     'text' : """
1068 When you add a user name to the list here,
1069 you initialize an entry telling fetchmail
1070 to poll the site on behalf of the new user.
1071
1072 When you select a username (by double-
1073 clicking it, or by single-clicking to
1074 select and then clicking the Edit button),
1075 you will open a window to configure the
1076 user's options on that site.
1077 """}
1078
1079 class ServerEdit(Frame, MyWidget):
1080     def __init__(self, host, parent):
1081         self.parent = parent
1082         self.server = None
1083         self.subwidgets = {}
1084         for site in parent.configuration.servers:
1085             if site.pollname == host:
1086                 self.server = site
1087         if (self.server == None):
1088                 self.server = Server()
1089                 self.server.pollname = host
1090                 self.server.via = None
1091                 parent.configuration.servers.append(self.server)
1092
1093     def edit(self, mode, master=None):
1094         Frame.__init__(self, master)
1095         Pack.config(self)
1096         self.master.title('Fetchmail host ' + self.server.pollname);
1097         self.master.iconname('Fetchmail host ' + self.server.pollname);
1098         self.post(Server, 'server')
1099         self.makeWidgets(self.server.pollname, mode)
1100         self.keepalive = []     # Use this to anchor the PhotoImage object
1101         make_icon_window(self, fetchmail_icon)
1102 #       self.grab_set()
1103 #       self.focus_set()
1104 #       self.wait_window()
1105         return self
1106
1107     def destruct(self):
1108         for username in self.subwidgets.keys():
1109             self.subwidgets[username].destruct()
1110         del self.parent.subwidgets[self.server.pollname]
1111         self.master.destroy()
1112
1113     def nosave(self):
1114         if ConfirmQuit(self, 'server option editing'):
1115             self.destruct()
1116
1117     def save(self):
1118         self.fetch(Server, 'server')
1119         for username in self.subwidgets.keys():
1120             self.subwidgets[username].save()
1121         self.destruct()
1122
1123     def defaultPort(self):
1124         proto = self.protocol.get()
1125         # Callback to reset the port number whenever the protocol type changes.
1126         # We used to only reset the port if it had a default (zero) value.
1127         # This turns out to be a bad idea especially in Novice mode -- if
1128         # you set POP3 and then set IMAP, the port invisibly remained 110.
1129         # Now we reset unconditionally on the theory that if you're setting
1130         # a custom port number you should be in expert mode and playing
1131         # close enough attention to notice this...
1132         self.service.set(defaultports[proto])
1133         if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED
1134
1135     def user_edit(self, username, mode):
1136         self.subwidgets[username] = UserEdit(username, self).edit(mode, Toplevel())
1137
1138     def user_delete(self, username):
1139         if self.subwidgets.has_key(username):
1140             self.subwidgets[username].destruct()
1141         del self.server[username]
1142
1143     def makeWidgets(self, host, mode):
1144         topwin = dispose_window(self, "Server options for querying " + host, serverhelp)
1145
1146         leftwin = Frame(self);
1147         leftwidth = '25';
1148
1149         if mode != 'novice':
1150             ctlwin = Frame(leftwin, relief=RAISED, bd=5)
1151             Label(ctlwin, text="Run Controls").pack(side=TOP)
1152             Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP)
1153             Checkbutton(ctlwin, text='Pass messages with bad headers?',
1154                     variable=self.badheader).pack(side=TOP)
1155             retrieveerrorlist = ['abort', 'continue', 'markseen']
1156             Label(ctlwin, text="Retrieve Error Policy").pack(side=TOP)
1157             ButtonBar(ctlwin, '', self.retrieveerror, retrieveerrorlist, 1, None)
1158             LabeledEntry(ctlwin, 'True name of ' + host + ':',
1159                       self.via, leftwidth).pack(side=TOP, fill=X)
1160             LabeledEntry(ctlwin, 'Cycles to skip between polls:',
1161                       self.interval, leftwidth).pack(side=TOP, fill=X)
1162             LabeledEntry(ctlwin, 'Server timeout (seconds):',
1163                       self.timeout, leftwidth).pack(side=TOP, fill=X)
1164             Button(ctlwin, text='Help', fg='blue',
1165                command=lambda: helpwin(controlhelp)).pack(side=RIGHT)
1166             ctlwin.pack(fill=X)
1167
1168         # Compute the available protocols from the compile-time options
1169         protolist = ['auto']
1170         if 'pop3' in feature_options:
1171             protolist = protolist + ["POP3", "APOP", "KPOP"]
1172         if 'sdps' in feature_options:
1173             protolist.append("SDPS")
1174         if 'imap' in feature_options:
1175             protolist.append("IMAP")
1176         if 'etrn' in feature_options:
1177             protolist.append("ETRN")
1178         if 'odmr' in feature_options:
1179             protolist.append("ODMR")
1180
1181         protwin = Frame(leftwin, relief=RAISED, bd=5)
1182         Label(protwin, text="Protocol").pack(side=TOP)
1183         ButtonBar(protwin, '',
1184                   self.protocol, protolist, 2,
1185                   self.defaultPort)
1186         if mode != 'novice':
1187             LabeledEntry(protwin, 'On server TCP/IP service:',
1188                       self.service, leftwidth).pack(side=TOP, fill=X)
1189             self.defaultPort()
1190             Checkbutton(protwin,
1191                 text="POP3: track `seen' with client-side UIDLs?",
1192                 variable=self.uidl).pack(side=TOP)
1193         Button(protwin, text='Probe for supported protocols', fg='blue',
1194                command=self.autoprobe).pack(side=LEFT)
1195         Button(protwin, text='Help', fg='blue',
1196                command=lambda: helpwin(protohelp)).pack(side=RIGHT)
1197         protwin.pack(fill=X)
1198
1199         userwin = Frame(leftwin, relief=RAISED, bd=5)
1200         Label(userwin, text="User entries for " + host).pack(side=TOP)
1201         ListEdit("New user: ",
1202                  map(lambda x: x.remote, self.server.users),
1203                  lambda u, m=mode, s=self: s.user_edit(u, m),
1204                  lambda u, s=self: s.user_delete(u),
1205                  userwin, suserhelp)
1206         userwin.pack(fill=X)
1207
1208         leftwin.pack(side=LEFT, anchor=N, fill=X);
1209
1210         if mode != 'novice':
1211             rightwin = Frame(self);
1212
1213             mdropwin = Frame(rightwin, relief=RAISED, bd=5)
1214             Label(mdropwin, text="Multidrop options").pack(side=TOP)
1215             LabeledEntry(mdropwin, 'Envelope address header:',
1216                       self.envelope, '22').pack(side=TOP, fill=X)
1217             LabeledEntry(mdropwin, 'Envelope headers to skip:',
1218                       self.envskip, '22').pack(side=TOP, fill=X)
1219             LabeledEntry(mdropwin, 'Name prefix to strip:',
1220                       self.qvirtual, '22').pack(side=TOP, fill=X)
1221             Checkbutton(mdropwin, text="Enable multidrop DNS lookup?",
1222                     variable=self.dns).pack(side=TOP)
1223             Label(mdropwin, text="DNS aliases").pack(side=TOP)
1224             ListEdit("New alias: ", self.server.aka, None, None, mdropwin, None)
1225             Label(mdropwin, text="Domains to be considered local").pack(side=TOP)
1226             ListEdit("New domain: ",
1227                  self.server.localdomains, None, None, mdropwin, multihelp)
1228             mdropwin.pack(fill=X)
1229
1230             if os_type in ('linux', 'freebsd'):
1231                 secwin = Frame(rightwin, relief=RAISED, bd=5)
1232                 Label(secwin, text="Security").pack(side=TOP)
1233                 # Don't actually let users set this.  KPOP sets it implicitly
1234                 ButtonBar(secwin, 'Authorization mode:',
1235                          self.auth, authlist, 2, None).pack(side=TOP)
1236                 if os_type == 'linux' or os_type == 'freebsd'  or 'interface' in dictmembers:
1237                     LabeledEntry(secwin, 'IP range to check before poll:',
1238                          self.interface, leftwidth).pack(side=TOP, fill=X)
1239                 if os_type == 'linux' or os_type == 'freebsd' or 'monitor' in dictmembers:
1240                     LabeledEntry(secwin, 'Interface to monitor:',
1241                          self.monitor, leftwidth).pack(side=TOP, fill=X)
1242                 # Someday this should handle Kerberos 5 too
1243                 if 'kerberos' in feature_options:
1244                     LabeledEntry(secwin, 'Principal:',
1245                          self.principal, '12').pack(side=TOP, fill=X)
1246                 # ESMTP authentication
1247                 LabeledEntry(secwin, 'ESMTP name:',
1248                              self.esmtpname, '12').pack(side=TOP, fill=X)
1249                 LabeledEntry(secwin, 'ESMTP password:',
1250                              self.esmtppassword, '12').pack(side=TOP, fill=X)
1251                 Button(secwin, text='Help', fg='blue',
1252                        command=lambda: helpwin(sechelp)).pack(side=RIGHT)
1253                 secwin.pack(fill=X)
1254
1255             rightwin.pack(side=LEFT, anchor=N);
1256
1257     def autoprobe(self):
1258         # Note: this only handles case (1) near fetchmail.c:1032
1259         # We're assuming people smart enough to set up ssh tunneling
1260         # won't need autoprobing.
1261         if self.server.via:
1262             realhost = self.server.via
1263         else:
1264             realhost = self.server.pollname
1265         greetline = None
1266         for protocol in ("IMAP","POP3"):
1267             service = defaultports[protocol]
1268             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1269             try:
1270                 sock.connect((realhost, ianaservices[service]))
1271                 greetline = sock.recv(1024)
1272                 sock.close()
1273             except:
1274                 pass
1275             else:
1276                 break
1277         confwin = Toplevel()
1278         if greetline == None:
1279             title = "Autoprobe of " + realhost + " failed"
1280             confirm = """
1281 Fetchmailconf didn't find any mailservers active.
1282 This could mean the host doesn't support any,
1283 or that your Internet connection is down, or
1284 that the host is so slow that the probe timed
1285 out before getting a response.
1286 """
1287         else:
1288             warnings = ''
1289             # OK, now try to recognize potential problems
1290
1291 ### POP3 servers start here
1292
1293             if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.004") > 0:
1294                 warnings = warnings + """
1295 This appears to be an old version of the UC Davis POP server.  These are
1296 dangerously unreliable (among other problems, they may drop your mailbox
1297 on the floor if your connection is interrupted during the session).
1298
1299 It is strongly recommended that you find a better POP3 server.  The fetchmail
1300 FAQ includes pointers to good ones.
1301
1302 """
1303             if string.find(greetline, "comcast.net") > 0:
1304                 warnings = warnings + """
1305 The Comcast Maillennium POP3 server only returns the first 80K of a long
1306 message retrieved with TOP. Its response to RETR is normal, so use the
1307 `fetchall' option.
1308
1309 """
1310 # Steve VanDevender <stevev@efn.org> writes:
1311 # The only system I have seen this happen with is cucipop-1.31
1312 # under SunOS 4.1.4.  cucipop-1.31 runs fine on at least Solaris
1313 # 2.x and probably quite a few other systems.  It appears to be a
1314 # bug or bad interaction with the SunOS realloc() -- it turns out
1315 # that internally cucipop does allocate a certain data structure in
1316 # multiples of 16, using realloc() to bump it up to the next
1317 # multiple if it needs more.
1318 #
1319 # The distinctive symptom is that when there are 16 messages in the
1320 # inbox, you can RETR and DELE all 16 messages successfully, but on
1321 # QUIT cucipop returns something like "-ERR Error locking your
1322 # mailbox" and aborts without updating it.
1323 #
1324 # The cucipop banner looks like:
1325 #
1326 # +OK Cubic Circle's v1.31 1998/05/13 POP3 ready <6229000062f95036@wakko>
1327 #
1328             if string.find(greetline, "Cubic Circle") > 0:
1329                 warnings = warnings + """
1330 I see your server is running cucipop.  Better make sure the server box
1331 isn't a SunOS 4.1.4 machine; cucipop tickles a bug in SunOS realloc()
1332 under that version, and doesn't cope with the result gracefully.  Newer
1333 SunOS and Solaris machines run cucipop OK.
1334
1335 Also, some versions of cucipop don't assert an exclusive lock on your
1336 mailbox when it's being queried.  This means that if you have more than
1337 one fetchmail query running against the same mailbox, bad things can happen.
1338 """
1339             if string.find(greetline, "David POP3 Server") > 0:
1340                 warnings = warnings + """
1341 This POP3 server is badly broken.  You should get rid of it -- and the
1342 brain-dead Microsoft operating system it rode in on.
1343
1344 """
1345 # The greeting line on the server known to be buggy is:
1346 # +OK POP3 server ready (running FTGate V2, 2, 1, 0 Jun 21 1999 09:55:01)
1347 #
1348             if string.find(greetline, "FTGate") > 0:
1349                 warnings = warnings + """
1350 This POP server has a weird bug; it says OK twice in response to TOP.
1351 Its response to RETR is normal, so use the `fetchall' option.
1352
1353 """
1354             if string.find(greetline, " geonet.de") > 0:
1355                 warnings = warnings + """
1356 You appear to be using geonet.  As of late 2002, the TOP command on
1357 geonet's POP3 is broken.  Use the fetchall option.
1358
1359 """
1360             if string.find(greetline, "OpenMail") > 0:
1361                 warnings = warnings + """
1362 You appear to be using some version of HP OpenMail.  Many versions of
1363 OpenMail do not process the "TOP" command correctly; the symptom is that
1364 only the header and first line of each message is retrieved.  To work
1365 around this bug, turn on `fetchall' on all user entries associated with
1366 this server.
1367
1368 """
1369             if string.find(greetline, "Escape character is") > 0:
1370                 warnings = warnings + """
1371 Your greeting line looks like it was written by a fetid pile of
1372 camel dung identified to me as `popa3d written by Solar Designer'.
1373 Beware!  The UIDL support in this thing is known to be completely broken,
1374 and other things probably are too.
1375
1376 """
1377             if string.find(greetline, "MercuryP/NLM v1.48") > 0:
1378                 warnings = warnings + """
1379 This is not a POP3 server.  It has delusions of being one, but after
1380 RETR all messages are automatically marked to be deleted.  The only
1381 way to prevent this is to issue an RSET before leaving the server.
1382 Fetchmail does this, but we suspect this is probably broken in lots
1383 of other ways, too.
1384
1385 """
1386             if string.find(greetline, "POP-Max") > 0:
1387                 warnings = warnings + """
1388 The Mail Max POP3 server screws up on mail with attachments.  It
1389 reports the message size with attachments included, but doesn't
1390 download them on a RETR or TOP (this violates the IMAP RFCs).  It also
1391 doesn't implement TOP correctly.  You should get rid of it -- and the
1392 brain-dead NT server it rode in on.
1393
1394 """
1395             if string.find(greetline, "POP3 Server Ready") > 0:
1396                 warnings = warnings + """
1397 Some server that uses this greeting line has been observed to choke on
1398 TOP %d 99999999.  Use the fetchall option. if necessary, to force RETR.
1399
1400 """
1401             if string.find(greetline, "QPOP") > 0:
1402                 warnings = warnings + """
1403 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1404 knows all about qpopper.  However, be aware that the 2.53 version of
1405 qpopper does something odd that causes fetchmail to hang with a socket
1406 error on very large messages.  This is probably not a fetchmail bug, as
1407 it has been observed with fetchpop.  The fix is to upgrade to qpopper
1408 3.0beta or a more recent version.  Better yet, switch to IMAP.
1409
1410 """
1411             if string.find(greetline, " sprynet.com") > 0:
1412                 warnings = warnings + """
1413 You appear to be using a SpryNet server.  In mid-1999 it was reported that
1414 the SpryNet TOP command marks messages seen.  Therefore, for proper error
1415 recovery in the event of a line drop, it is strongly recommended that you
1416 turn on `fetchall' on all user entries associated with this server.
1417
1418 """
1419             if string.find(greetline, "TEMS POP3") > 0:
1420                 warnings = warnings + """
1421 Your POP3 server has "TEMS" in its header line.  At least one such
1422 server does not process the "TOP" command correctly; the symptom is
1423 that fetchmail hangs when trying to retrieve mail.  To work around
1424 this bug, turn on `fetchall' on all user entries associated with this
1425 server.
1426
1427 """
1428             if string.find(greetline, " spray.se") > 0:
1429                 warnings = warnings + """
1430 Your POP3 server has "spray.se" in its header line.  In May 2000 at
1431 least one such server did not process the "TOP" command correctly; the
1432 symptom is that messages are treated as headerless.  To work around
1433 this bug, turn on `fetchall' on all user entries associated with this
1434 server.
1435
1436 """
1437             if string.find(greetline, " usa.net") > 0:
1438                 warnings = warnings + """
1439 You appear to be using USA.NET's free mail service.  Their POP3 servers
1440 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
1441 fetchmail can compensate.  They seem to require that fetchall be switched on
1442 (otherwise you won't necessarily see all your mail, not even new mail).
1443 They also botch the TOP command the fetchmail normally uses for retrieval
1444 (it only retrieves about 10 lines rather than the number specified).
1445 Turning on fetchall will disable the use of TOP.
1446
1447 Therefore, it is strongly recommended that you turn on `fetchall' on all
1448 user entries associated with this server.
1449
1450 """
1451             if string.find(greetline, " Novonyx POP3") > 0:
1452                 warnings = warnings + """
1453 Your mailserver is running Novonyx POP3.  This server, at least as of
1454 version 2.17, seems to have problems handling and reporting seen bits.
1455 You may have to use the fetchall option.
1456
1457 """
1458             if string.find(greetline, " IMS POP3") > 0:
1459                 warnings = warnings + """
1460 Some servers issuing the greeting line 'IMS POP3' have been known to
1461 do byte-stuffing incorrectly.  This means that if a message you receive
1462 has a . (period) at start of line, fetchmail will become confused and
1463 probably wedge itself.  (This bug was recorded on IMS POP3 0.86.)
1464
1465 """
1466
1467 ### IMAP servers start here
1468
1469             if string.find(greetline, "GroupWise") > 0:
1470                 warnings = warnings + """
1471 The Novell GroupWise IMAP server would be better named GroupFoolish;
1472 it is (according to the designer of IMAP) unusably broken.  Among
1473 other things, it doesn't include a required content length in its
1474 BODY[TEXT] response.<p>
1475
1476 Fetchmail works around this problem, but we strongly recommend voting
1477 with your dollars for a server that isn't brain-dead.  If you stick
1478 with code as shoddy as GroupWise seems to be, you will probably pay
1479 for it with other problems.<p>
1480
1481 """
1482             if string.find(greetline, "InterChange") > 0:
1483                 warnings = warnings + """
1484
1485 The InterChange IMAP server at release levels below 3.61.08 screws up
1486 on mail with attachments.  It doesn't fetch them if you give it a
1487 BODY[TEXT] request, though it does if you request RFC822.TEXT.
1488 According to the IMAP RFCs and their maintainer these should be
1489 equivalent -- and we can't drop the BODY[TEXT] form because M$
1490 Exchange (quite legally under RFC2062) rejectsit.  The InterChange
1491 folks claim to have fixed this bug in 3.61.08.
1492
1493 """
1494             if string.find(greetline, "Imail") > 0:
1495                 warnings = warnings + """
1496 We've seen a bug report indicating that this IMAP server (at least as of
1497 version 5.0.7) returns an invalid body size for messages with MIME
1498 attachments; the effect is to drop the attachments on the floor.  We
1499 recommend you upgrade to a non-broken IMAP server.
1500
1501 """
1502             if string.find(greetline, "Domino IMAP4") > 0:
1503                 warnings = warnings + """
1504 Your IMAP server appears to be Lotus Domino.  This server, at least up
1505 to version 4.6.2a, has a bug in its generation of MIME boundaries (see
1506 the details in the fetchmail FAQ).  As a result, even MIME aware MUAs
1507 will see attachments as part of the message text.  If your Domino server's
1508 POP3 facility is enabled, we recommend you fall back on it.
1509
1510 """
1511
1512 ### Checks for protocol variants start here
1513
1514             closebrak = string.find(greetline, ">")
1515             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1516                 warnings = warnings + """
1517 It looks like you could use APOP on this server and avoid sending it your
1518 password in clear.  You should talk to the mailserver administrator about
1519 this.
1520
1521 """
1522             if string.find(greetline, "IMAP2bis") > 0:
1523                 warnings = warnings + """
1524 IMAP2bis servers have a minor problem; they can't peek at messages without
1525 marking them seen.  If you take a line hit during the retrieval, the
1526 interrupted message may get left on the server, marked seen.
1527
1528 To work around this, it is recommended that you set the `fetchall'
1529 option on all user entries associated with this server, so any stuck
1530 mail will be retrieved next time around.
1531
1532 To fix this bug, upgrade to an IMAP4 server.  The fetchmail FAQ includes
1533 a pointer to an open-source implementation.
1534
1535 """
1536             if string.find(greetline, "IMAP4rev1") > 0:
1537                 warnings = warnings + """
1538 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1539 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1540 has therefore been extremely well tested with this class of server.
1541
1542 """
1543             if warnings == '':
1544                 warnings = warnings + """
1545 Fetchmail doesn't know anything special about this server type.
1546
1547 """
1548
1549             # Display success window with warnings
1550             title = "Autoprobe of " + realhost + " succeeded"
1551             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1552             self.protocol.set(protocol)
1553             self.service.set(defaultports[protocol])
1554         confwin.title(title)
1555         confwin.iconname(title)
1556         Label(confwin, text=title).pack()
1557         Message(confwin, text=confirm, width=600).pack()
1558         Button(confwin, text='Done',
1559                    command=lambda x=confwin: x.destroy(), bd=2).pack()
1560
1561 #
1562 # User editing stuff
1563 #
1564
1565 userhelp = {
1566     'title' : 'User option help',
1567     'banner': 'User options',
1568     'text' : """
1569 You may use this panel to set options
1570 that may differ between individual
1571 users on your site.
1572
1573 Once you have a user configuration set
1574 up as you like it, you can select `OK' to
1575 store it in the user list maintained in
1576 the site configuration window.
1577
1578 If you wish to discard the changes you have
1579 made to user options, select `Quit'.
1580 """}
1581
1582 localhelp = {
1583     'title' : 'Local name help',
1584     'banner': 'Local names',
1585     'text' : """
1586 The local name(s) in a user entry are the
1587 people on the client machine who should
1588 receive mail from the poll described.
1589
1590 Note: if a user entry has more than one
1591 local name, messages will be retrieved
1592 in multidrop mode.  This complicates
1593 the configuration issues; see the manual
1594 page section on multidrop mode.
1595
1596 Warning: Be careful with local names
1597 such as foo@bar.com, as that can cause
1598 the mail to be sent to foo@bar.com instead
1599 of sending it to your local system.
1600 """}
1601
1602 class UserEdit(Frame, MyWidget):
1603     def __init__(self, username, parent):
1604         self.parent = parent
1605         self.user = None
1606         for user in parent.server.users:
1607             if user.remote == username:
1608                 self.user = user
1609         if self.user == None:
1610             self.user = User()
1611             self.user.remote = username
1612             self.user.localnames = [username]
1613             parent.server.users.append(self.user)
1614
1615     def edit(self, mode, master=None):
1616         Frame.__init__(self, master)
1617         Pack.config(self)
1618         self.master.title('Fetchmail user ' + self.user.remote
1619                           + ' querying ' + self.parent.server.pollname);
1620         self.master.iconname('Fetchmail user ' + self.user.remote);
1621         self.post(User, 'user')
1622         self.makeWidgets(mode, self.parent.server.pollname)
1623         self.keepalive = []     # Use this to anchor the PhotoImage object
1624         make_icon_window(self, fetchmail_icon)
1625 #       self.grab_set()
1626 #       self.focus_set()
1627 #       self.wait_window()
1628         return self
1629
1630     def destruct(self):
1631         # Yes, this test can fail -- if you delete the parent window.
1632         if self.parent.subwidgets.has_key(self.user.remote):
1633             del self.parent.subwidgets[self.user.remote]
1634         self.master.destroy()
1635
1636     def nosave(self):
1637         if ConfirmQuit(self, 'user option editing'):
1638             self.destruct()
1639
1640     def save(self):
1641         ok = 0
1642         for x in self.user.localnames: ok = ok + (string.find(x, '@') != -1)
1643         if ok == 0 or  Dialog(self,
1644             title = "Really accept an embedded '@' ?",
1645             text = "Local names with an embedded '@', such as in foo@bar "
1646                    "might result in your mail being sent to foo@bar.com "
1647                    "instead of your local system.\n Are you sure you want "
1648                    "a local user name with an '@' in it?",
1649             bitmap = 'question',
1650             strings = ('Yes', 'No'),
1651             default = 1).num == 0:
1652                 self.fetch(User, 'user')
1653                 self.destruct()
1654
1655     def makeWidgets(self, mode, servername):
1656         dispose_window(self,
1657                         "User options for " + self.user.remote + " querying " + servername,
1658                         userhelp)
1659
1660         if mode != 'novice':
1661             leftwin = Frame(self);
1662         else:
1663             leftwin = self
1664
1665         secwin = Frame(leftwin, relief=RAISED, bd=5)
1666         Label(secwin, text="Authentication").pack(side=TOP)
1667         LabeledEntry(secwin, 'Password:',
1668                       self.password, '12').pack(side=TOP, fill=X)
1669         secwin.pack(fill=X, anchor=N)
1670
1671         if 'ssl' in feature_options or 'ssl' in dictmembers:
1672             sslwin = Frame(leftwin, relief=RAISED, bd=5)
1673             Checkbutton(sslwin, text="Use SSL?",
1674                         variable=self.ssl).pack(side=TOP, fill=X)
1675             LabeledEntry(sslwin, 'SSL key:',
1676                          self.sslkey, '14').pack(side=TOP, fill=X)
1677             LabeledEntry(sslwin, 'SSL certificate:',
1678                          self.sslcert, '14').pack(side=TOP, fill=X)
1679             Checkbutton(sslwin, text="Check server SSL certificate?",
1680                         variable=self.sslcertck).pack(side=TOP, fill=X)
1681             LabeledEntry(sslwin, 'SSL trusted certificate directory:',
1682                          self.sslcertpath, '14').pack(side=TOP, fill=X)
1683             LabeledEntry(sslwin, 'SSL CommonName:',
1684                          self.sslcommonname, '14').pack(side=TOP, fill=X)
1685             LabeledEntry(sslwin, 'SSL key fingerprint:',
1686                          self.sslfingerprint, '14').pack(side=TOP, fill=X)
1687             sslwin.pack(fill=X, anchor=N)
1688
1689         names = Frame(leftwin, relief=RAISED, bd=5)
1690         Label(names, text="Local names").pack(side=TOP)
1691         ListEdit("New name: ",
1692                      self.user.localnames, None, None, names, localhelp)
1693         names.pack(fill=X, anchor=N)
1694
1695         if mode != 'novice':
1696             targwin = Frame(leftwin, relief=RAISED, bd=5)
1697             Label(targwin, text="Forwarding Options").pack(side=TOP)
1698             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1699             ListEdit("New listener:",
1700                      self.user.smtphunt, None, None, targwin, None)
1701             Label(targwin, text="Domains to fetch from (ODMR/ETRN only)").pack(side=TOP)
1702             ListEdit("Domains:",
1703                      self.user.fetchdomains, None, None, targwin, None)
1704             LabeledEntry(targwin, 'Use domain on RCPT TO line:',
1705                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1706             LabeledEntry(targwin, 'Set fixed RCPT TO address:',
1707                      self.smtpname, '26').pack(side=TOP, fill=X)
1708             LabeledEntry(targwin, 'Connection setup command:',
1709                      self.preconnect, '26').pack(side=TOP, fill=X)
1710             LabeledEntry(targwin, 'Connection wrapup command:',
1711                      self.postconnect, '26').pack(side=TOP, fill=X)
1712             LabeledEntry(targwin, 'Local delivery agent:',
1713                      self.mda, '26').pack(side=TOP, fill=X)
1714             LabeledEntry(targwin, 'BSMTP output file:',
1715                      self.bsmtp, '26').pack(side=TOP, fill=X)
1716             LabeledEntry(targwin, 'Listener spam-block codes:',
1717                      self.antispam, '26').pack(side=TOP, fill=X)
1718             LabeledEntry(targwin, 'Pass-through properties:',
1719                      self.properties, '26').pack(side=TOP, fill=X)
1720             Checkbutton(targwin, text="Use LMTP?",
1721                         variable=self.lmtp).pack(side=TOP, fill=X)
1722             targwin.pack(fill=X, anchor=N)
1723
1724         if mode != 'novice':
1725             leftwin.pack(side=LEFT, fill=X, anchor=N)
1726             rightwin = Frame(self)
1727         else:
1728             rightwin = self
1729
1730         optwin = Frame(rightwin, relief=RAISED, bd=5)
1731         Label(optwin, text="Processing Options").pack(side=TOP)
1732         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1733                     variable=self.keep).pack(side=TOP, anchor=W)
1734         Checkbutton(optwin, text="Fetch old messages as well as new",
1735                     variable=self.fetchall).pack(side=TOP, anchor=W)
1736         if mode != 'novice':
1737             Checkbutton(optwin, text="Flush seen messages before retrieval",
1738                     variable=self.flush).pack(side=TOP, anchor=W)
1739             Checkbutton(optwin, text="Flush oversized messages before retrieval",
1740                     variable=self.limitflush).pack(side=TOP, anchor=W)
1741             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply",
1742                     variable=self.rewrite).pack(side=TOP, anchor=W)
1743             Checkbutton(optwin, text="Force CR/LF at end of each line",
1744                     variable=self.forcecr).pack(side=TOP, anchor=W)
1745             Checkbutton(optwin, text="Strip CR from end of each line",
1746                     variable=self.stripcr).pack(side=TOP, anchor=W)
1747             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1748                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1749             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1750                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1751             Checkbutton(optwin, text="Drop Status lines from forwarded messages",
1752                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1753             Checkbutton(optwin, text="Drop Delivered-To lines from forwarded messages",
1754                     variable=self.dropdelivered).pack(side=TOP, anchor=W)
1755         optwin.pack(fill=X)
1756
1757         if mode != 'novice':
1758             limwin = Frame(rightwin, relief=RAISED, bd=5)
1759             Label(limwin, text="Resource Limits").pack(side=TOP)
1760             LabeledEntry(limwin, 'Message size limit:',
1761                       self.limit, '30').pack(side=TOP, fill=X)
1762             LabeledEntry(limwin, 'Size warning interval:',
1763                       self.warnings, '30').pack(side=TOP, fill=X)
1764             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1765                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1766             LabeledEntry(limwin, 'Max message sizes to fetch per transaction:',
1767                       self.fetchsizelimit, '30').pack(side=TOP, fill=X)
1768             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1769                 LabeledEntry(limwin, 'Use fast UIDL:',
1770                         self.fastuidl, '30').pack(side=TOP, fill=X)
1771             LabeledEntry(limwin, 'Max messages to forward per poll:',
1772                       self.batchlimit, '30').pack(side=TOP, fill=X)
1773             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1774                 LabeledEntry(limwin, 'Interval between expunges:',
1775                              self.expunge, '30').pack(side=TOP, fill=X)
1776             Checkbutton(limwin, text="Idle after each poll (IMAP only)",
1777                     variable=self.idle).pack(side=TOP, anchor=W)
1778             limwin.pack(fill=X)
1779
1780             if self.parent.server.protocol == 'IMAP':
1781                 foldwin = Frame(rightwin, relief=RAISED, bd=5)
1782                 Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1783                 ListEdit("New folder:", self.user.mailboxes,
1784                          None, None, foldwin, None)
1785                 foldwin.pack(fill=X, anchor=N)
1786
1787         if mode != 'novice':
1788             rightwin.pack(side=LEFT)
1789         else:
1790             self.pack()
1791
1792
1793 #
1794 # Top-level window that offers either novice or expert mode
1795 # (but not both at once; it disappears when one is selected).
1796 #
1797
1798 class Configurator(Frame):
1799     def __init__(self, outfile, master, onexit, parent):
1800         Frame.__init__(self, master)
1801         self.outfile = outfile
1802         self.onexit = onexit
1803         self.parent = parent
1804         self.master.title('fetchmail configurator');
1805         self.master.iconname('fetchmail configurator');
1806         Pack.config(self)
1807         self.keepalive = []     # Use this to anchor the PhotoImage object
1808         make_icon_window(self, fetchmail_icon)
1809
1810         Message(self, text="""
1811 Use `Novice Configuration' for basic fetchmail setup;
1812 with this, you can easily set up a single-drop connection
1813 to one remote mail server.
1814 """, width=600).pack(side=TOP)
1815         Button(self, text='Novice Configuration',
1816                                 fg='blue', command=self.novice).pack()
1817
1818         Message(self, text="""
1819 Use `Expert Configuration' for advanced fetchmail setup,
1820 including multiple-site or multidrop connections.
1821 """, width=600).pack(side=TOP)
1822         Button(self, text='Expert Configuration',
1823                                 fg='blue', command=self.expert).pack()
1824
1825         Message(self, text="""
1826 Or you can just select `Quit' to leave the configurator now and
1827 return to the main panel.
1828 """, width=600).pack(side=TOP)
1829         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1830         master.protocol("WM_DELETE_WINDOW", self.leave)
1831
1832     def novice(self):
1833         self.master.destroy()
1834         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('novice')
1835
1836     def expert(self):
1837         self.master.destroy()
1838         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('expert')
1839
1840     def leave(self):
1841         self.master.destroy()
1842         self.onexit()
1843
1844 # Run a command in a scrolling text widget, displaying its output
1845
1846 class RunWindow(Frame):
1847     def __init__(self, command, master, parent):
1848         Frame.__init__(self, master)
1849         self.master = master
1850         self.master.title('fetchmail run window');
1851         self.master.iconname('fetchmail run window');
1852         Pack.config(self)
1853         Label(self,
1854                 text="Running "+command,
1855                 bd=2).pack(side=TOP, pady=10)
1856         self.keepalive = []     # Use this to anchor the PhotoImage object
1857         make_icon_window(self, fetchmail_icon)
1858
1859         # This is a scrolling text window
1860         textframe = Frame(self)
1861         scroll = Scrollbar(textframe)
1862         self.textwidget = Text(textframe, setgrid=TRUE)
1863         textframe.pack(side=TOP, expand=YES, fill=BOTH)
1864         self.textwidget.config(yscrollcommand=scroll.set)
1865         self.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
1866         scroll.config(command=self.textwidget.yview)
1867         scroll.pack(side=RIGHT, fill=BOTH)
1868         textframe.pack(side=TOP)
1869
1870         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1871
1872         self.update()   # Draw widget before executing fetchmail
1873
1874         # Always look for a runnable command in the directory we're running in
1875         # first. This avoids some obscure version-skew errors that can occur
1876         # if you pick up an old fetchmail from the standard system locations.
1877         os.environ["PATH"] = os.path.dirname(sys.argv[0]) + ":" + os.environ["PATH"]
1878         child_stdout = os.popen(command + " 2>&1 </dev/null", "r")
1879         while 1:
1880             ch = child_stdout.read(1)
1881             if not ch:
1882                 break
1883             self.textwidget.insert(END, ch)
1884         self.textwidget.insert(END, "Done.")
1885         self.textwidget.see(END);
1886
1887     def leave(self):
1888         self.master.destroy()
1889
1890 # Here's where we choose either configuration or launching
1891
1892 class MainWindow(Frame):
1893     def __init__(self, outfile, master=None):
1894         Frame.__init__(self, master)
1895         self.outfile = outfile
1896         self.master.title('fetchmail launcher');
1897         self.master.iconname('fetchmail launcher');
1898         Pack.config(self)
1899         Label(self,
1900                 text='Fetchmailconf ' + version,
1901                 bd=2).pack(side=TOP, pady=10)
1902         self.keepalive = []     # Use this to anchor the PhotoImage object
1903         make_icon_window(self, fetchmail_icon)
1904         self.debug = 0
1905
1906         ## Test icon display with the following:
1907         # icon_image = PhotoImage(data=fetchmail_icon)
1908         # Label(self, image=icon_image).pack(side=TOP, pady=10)
1909         # self.keepalive.append(icon_image)
1910
1911         Message(self, text="""
1912 Use `Configure fetchmail' to tell fetchmail about the remote
1913 servers it should poll (the host name, your username there,
1914 whether to use POP or IMAP, and so forth).
1915 """, width=600).pack(side=TOP)
1916         self.configbutton = Button(self, text='Configure fetchmail',
1917                                 fg='blue', command=self.configure)
1918         self.configbutton.pack()
1919
1920         Message(self, text="""
1921 Use `Run fetchmail' to run fetchmail with debugging enabled.
1922 This is a good way to test out a new configuration.
1923 """, width=600).pack(side=TOP)
1924         Button(self, text='Run fetchmail',fg='blue', command=self.test).pack()
1925
1926         Message(self, text="""
1927 Use `Run fetchmail' to run fetchmail in foreground.
1928 Progress  messages will be shown, but not debug messages.
1929 """, width=600).pack(side=TOP)
1930         Button(self, text='Run fetchmail', fg='blue', command=self.run).pack()
1931
1932         Message(self, text="""
1933 Or you can just select `Quit' to exit the launcher now.
1934 """, width=600).pack(side=TOP)
1935         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1936
1937     def configure(self):
1938         self.configbutton.configure(state=DISABLED)
1939         Configurator(self.outfile, Toplevel(),
1940                      lambda self=self: self.configbutton.configure(state=NORMAL),
1941                      self)
1942     def test(self):
1943         cmd = "fetchmail -N -d0 --nosyslog -v"
1944         if rcfile:
1945             cmd = cmd + " -f " + rcfile
1946         RunWindow(cmd, Toplevel(), self)
1947
1948     def run(self):
1949         cmd = "fetchmail -N -d0"
1950         if rcfile:
1951             cmd = cmd + " -f " + rcfile
1952         RunWindow(cmd, Toplevel(), self)
1953
1954     def leave(self):
1955         self.quit()
1956
1957 # Functions for turning a dictionary into an instantiated object tree.
1958
1959 def intersect(list1, list2):
1960 # Compute set intersection of lists
1961     res = []
1962     for x in list1:
1963         if x in list2:
1964             res.append(x)
1965     return res
1966
1967 def setdiff(list1, list2):
1968 # Compute set difference of lists
1969     res = []
1970     for x in list1:
1971         if not x in list2:
1972             res.append(x)
1973     return res
1974
1975 def copy_instance(toclass, fromdict):
1976 # Initialize a class object of given type from a conformant dictionary.
1977     for fld in fromdict.keys():
1978         if not fld in dictmembers:
1979             dictmembers.append(fld)
1980 # The `optional' fields are the ones we can ignore for purposes of
1981 # conformability checking; they'll still get copied if they are
1982 # present in the dictionary.
1983     optional = ('interface', 'monitor',
1984                 'esmtpname', 'esmtppassword',
1985                 'ssl', 'sslkey', 'sslcert', 'sslproto', 'sslcertck',
1986                 'sslcertpath', 'sslcommonname', 'sslfingerprint', 'showdots')
1987     class_sig = setdiff(toclass.__dict__.keys(), optional)
1988     class_sig.sort()
1989     dict_keys = setdiff(fromdict.keys(), optional)
1990     dict_keys.sort()
1991     common = intersect(class_sig, dict_keys)
1992     if 'typemap' in class_sig:
1993         class_sig.remove('typemap')
1994     if tuple(class_sig) != tuple(dict_keys):
1995         print "Fields don't match what fetchmailconf expected:"
1996 #       print "Class signature: " + `class_sig`
1997 #       print "Dictionary keys: " + `dict_keys`
1998         diff = setdiff(class_sig, common)
1999         if diff:
2000             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
2001         diff = setdiff(dict_keys, common)
2002         if diff:
2003             print "Not matched in dictionary keys: " + `diff`
2004         sys.exit(1)
2005     else:
2006         for x in fromdict.keys():
2007             setattr(toclass, x, fromdict[x])
2008
2009 #
2010 # And this is the main sequence.  How it works:
2011 #
2012 # First, call `fetchmail --configdump' and trap the output in a tempfile.
2013 # This should fill it with a Python initializer for a variable `fetchmailrc'.
2014 # Run execfile on the file to pull fetchmailrc into Python global space.
2015 # You don't want static data, though; you want, instead, a tree of objects
2016 # with the same data members and added appropriate methods.
2017 #
2018 # This is what the copy_instance function() is for.  It tries to copy a
2019 # dictionary field by field into a class, aborting if the class and dictionary
2020 # have different data members (except for any typemap member in the class;
2021 # that one is strictly for use by the MyWidget supperclass).
2022 #
2023 # Once the object tree is set up, require user to choose novice or expert
2024 # mode and instantiate an edit object for the configuration.  Class methods
2025 # will take it all from there.
2026 #
2027 # Options (not documented because they're for fetchmailconf debuggers only):
2028 # -d: Read the configuration and dump it to stdout before editing.  Dump
2029 #     the edited result to stdout as well.
2030 # -f: specify the run control file to read.
2031
2032 if __name__ == '__main__':
2033
2034     if not os.environ.has_key("DISPLAY"):
2035         print "fetchmailconf must be run under X"
2036         sys.exit(1)
2037
2038     fetchmail_icon = """
2039 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
2040 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
2041 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
2042 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
2043 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
2044 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
2045 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
2046 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
2047 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
2048 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
2049 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
2050 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
2051 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
2052 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
2053 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
2054 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
2055 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
2056 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
2057 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
2058 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
2059 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
2060 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
2061 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
2062 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
2063 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
2064 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
2065 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
2066 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
2067 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
2068 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
2069 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
2070 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
2071 """
2072 # The base64 data in the string above was generated by the following procedure:
2073 #
2074 # import base64
2075 # print base64.encodestring(open("fetchmail.gif", "rb").read())
2076 #
2077
2078     # Process options
2079     (options, arguments) = getopt.getopt(sys.argv[1:], "df:hV", ["help",
2080             "version"])
2081     dump = rcfile = None;
2082     for (switch, val) in options:
2083         if (switch == '-d'):
2084             dump = TRUE
2085         elif (switch == '-f'):
2086             rcfile = val
2087         elif (switch == '-h' or switch == '--help'):
2088             print """
2089 Usage: fetchmailconf {[-d] [-f fetchmailrc]|-h|--help|-V|--version}
2090            -d      - dump configuration (for debugging)
2091            -f fmrc - read alternate fetchmailrc file
2092 --help,    -h      - print this help text and quit
2093 --version, -V      - print fetchmailconf version and quit
2094 """
2095             sys.exit(0)
2096         elif (switch == '-V' or switch == '--version'):
2097             print "fetchmailconf %s" % version
2098             print """
2099 Copyright (C) 1997 - 2003 Eric S. Raymond
2100 Copyright (C) 2005, 2006, 2008, 2009 Matthias Andree
2101 fetchmailconf comes with ABSOLUTELY NO WARRANTY.  This is free software, you are
2102 welcome to redistribute it under certain conditions.  Please see the file
2103 COPYING in the source or documentation directory for details."""
2104             sys.exit(0)
2105
2106     # Get client host's FQDN
2107     hostname = socket.gethostbyaddr(socket.gethostname())[0]
2108
2109     # Compute defaults
2110     ConfigurationDefaults = Configuration()
2111     ServerDefaults = Server()
2112     UserDefaults = User()
2113
2114     # Read the existing configuration.  We set the umask to 077 to make sure
2115     # that group & other read/write permissions are shut off -- we wouldn't
2116     # want crackers to snoop password information out of the tempfile.
2117     tmpfile = tempfile.mktemp()
2118     if rcfile:
2119         cmd = "umask 077 && fetchmail </dev/null -f " + rcfile + " --configdump --nosyslog >" + tmpfile
2120     else:
2121         cmd = "umask 077 && fetchmail </dev/null --configdump --nosyslog >" + tmpfile
2122
2123     try:
2124         s = os.system(cmd)
2125         if s != 0:
2126             print "`" + cmd + "' run failure, status " + `s`
2127             raise SystemExit
2128     except:
2129         print "Unknown error while running fetchmail --configdump"
2130         os.remove(tmpfile)
2131         sys.exit(1)
2132
2133     try:
2134         execfile(tmpfile)
2135     except:
2136         print "Can't read configuration output of fetchmail --configdump."
2137         os.remove(tmpfile)
2138         sys.exit(1)
2139
2140     os.remove(tmpfile)
2141
2142     # The tricky part -- initializing objects from the configuration global
2143     # `Configuration' is the top level of the object tree we're going to mung.
2144     # The dictmembers list is used to track the set of fields the dictionary
2145     # contains; in particular, we can use it to tell whether things like the
2146     # monitor, interface, ssl, sslkey, or sslcert fields are present.
2147     dictmembers = []
2148     Fetchmailrc = Configuration()
2149     copy_instance(Fetchmailrc, fetchmailrc)
2150     Fetchmailrc.servers = [];
2151     for server in fetchmailrc['servers']:
2152         Newsite = Server()
2153         copy_instance(Newsite, server)
2154         Fetchmailrc.servers.append(Newsite)
2155         Newsite.users = [];
2156         for user in server['users']:
2157             Newuser = User()
2158             copy_instance(Newuser, user)
2159             Newsite.users.append(Newuser)
2160
2161     # We may want to display the configuration and quit
2162     if dump:
2163         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
2164
2165     # The theory here is that -f alone sets the rcfile location,
2166     # but -d and -f together mean the new configuration should go to stdout.
2167     if not rcfile and not dump:
2168         rcfile = os.environ["HOME"] + "/.fetchmailrc"
2169
2170     # OK, now run the configuration edit
2171     root = MainWindow(rcfile)
2172     root.mainloop()
2173
2174 # The following sets edit modes for GNU EMACS
2175 # Local Variables:
2176 # mode:python
2177 # End: