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