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