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