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