]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf
First round ofmlong-delayed bug fixes.
[~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.43"
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 = ""      # 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", "otp")
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, "OpenMail") > 0:
1290                 warnings = warnings + """
1291 You appear to be using some version of HP OpenMail.  Many versions of
1292 OpenMail do not process the "TOP" command correctly; the symptom is that
1293 only the header and first line of each message is retrieved.  To work
1294 around this bug, turn on `fetchall' on all user entries associated with
1295 this server.  
1296
1297 """
1298             if string.find(greetline, "Escape character is") > 0:
1299                 warnings = warnings + """
1300 Your greeting line looks like it was written by a fetid pile of
1301 camel dung identified to me as `popa3d written by Solar Designer'.
1302 Beware!  The UIDL support in this thing is known to be completely broken,
1303 and other things probably are too.
1304
1305 """
1306             if string.find(greetline, "MercuryP/NLM v1.48") > 0:
1307                 warnings = warnings + """
1308 This is not a POP3 server.  It has delusions of being one, but after
1309 RETR all messages are automatically marked to be deleted.  The only
1310 way to prevent this is to issue an RSET before leaving the server.
1311 Fetchmail does this, but we suspect this is probably broken in lots
1312 of other ways, too.
1313
1314 """
1315             if string.find(greetline, "POP-Max") > 0:
1316                 warnings = warnings + """
1317 The Mail Max POP3 server screws up on mail with attachments.  It
1318 reports the message size with attachments included, but doesn't
1319 download them on a RETR or TOP (this violates the IMAP RFCs).  It also
1320 doesn't implement TOP correctly.  You should get rid of it -- and the
1321 brain-dead NT server it rode in on.
1322
1323 """
1324             if string.find(greetline, "POP3 Server Ready") > 0:
1325                 warnings = warnings + """
1326 Some server that uses this greeting line has been observed to choke on
1327 TOP %d 99999999.  Use the fetchall option. if necessary, to force RETR.
1328
1329 """
1330             if string.find(greetline, "QPOP") > 0:
1331                 warnings = warnings + """
1332 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1333 knows all about qpopper.  However, be aware that the 2.53 version of
1334 qpopper does something odd that causes fetchmail to hang with a socket
1335 error on very large messages.  This is probably not a fetchmail bug, as
1336 it has been observed with fetchpop.  The fix is to upgrade to qpopper
1337 3.0beta or a more recent version.  Better yet, switch to IMAP.
1338
1339 """
1340             if string.find(greetline, " sprynet.com") > 0:
1341                 warnings = warnings + """
1342 You appear to be using a SpryNet server.  In mid-1999 it was reported that
1343 the SpryNet TOP command marks messages seen.  Therefore, for proper error
1344 recovery in the event of a line drop, it is strongly recommended that you
1345 turn on `fetchall' on all user entries associated with this server.  
1346
1347 """
1348             if string.find(greetline, "TEMS POP3") > 0:
1349                 warnings = warnings + """
1350 Your POP3 server has "TEMS" in its header line.  At least one such
1351 server does not process the "TOP" command correctly; the symptom is
1352 that fetchmail hangs when trying to retrieve mail.  To work around
1353 this bug, turn on `fetchall' on all user entries associated with this
1354 server.
1355
1356 """
1357             if string.find(greetline, " spray.se") > 0:
1358                 warnings = warnings + """
1359 Your POP3 server has "spray.se" in its header line.  In May 2000 at
1360 least one such server did not process the "TOP" command correctly; the
1361 symptom is that messages are treated as headerless.  To work around
1362 this bug, turn on `fetchall' on all user entries associated with this
1363 server.
1364
1365 """
1366             if string.find(greetline, " usa.net") > 0:
1367                 warnings = warnings + """
1368 You appear to be using USA.NET's free mail service.  Their POP3 servers
1369 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
1370 fetchmail can compensate.  They seem to require that fetchall be switched on
1371 (otherwise you won't necessarily see all your mail, not even new mail).
1372 They also botch the TOP command the fetchmail normally uses for retrieval
1373 (it only retrieves about 10 lines rather than the number specified).
1374 Turning on fetchall will disable the use of TOP.
1375
1376 Therefore, it is strongly recommended that you turn on `fetchall' on all
1377 user entries associated with this server.  
1378
1379 """
1380             if string.find(greetline, " Novonyx POP3") > 0:
1381                 warnings = warnings + """
1382 Your mailserver is running Novonyx POP3.  This server, at least as of
1383 version 2.17, seems to have problems handling and reporting seen bits.
1384 You may have to use the fetchall option.
1385
1386 """
1387             if string.find(greetline, " IMS POP3") > 0:
1388                 warnings = warnings + """
1389 Some servers issuing the greeting line 'IMS POP3' have been known to
1390 do byte-stuffing incorrectly.  This means that if a message you receive
1391 has a . (period) at start of line, fetchmail will become confused and
1392 probably wedge itself.  (This bug was recorded on IMS POP3 0.86.)
1393
1394 """
1395
1396 ### IMAP servers start here
1397
1398             if string.find(greetline, "GroupWise") > 0:
1399                 warnings = warnings + """
1400 The Novell GroupWise IMAP server would be better named GroupFoolish;
1401 it is (according to the designer of IMAP) unusably broken.  Among
1402 other things, it doesn't include a required content length in its
1403 BODY[TEXT] response.<p>
1404
1405 Fetchmail works around this problem, but we strongly recommend voting
1406 with your dollars for a server that isn't brain-dead.  If you stick
1407 with code as shoddy as GroupWise seems to be, you will probably pay
1408 for it with other problems.<p>
1409
1410 """
1411             if string.find(greetline, "Netscape IMAP4rev1 Service 3.6") > 0:
1412                 warnings = warnings + """
1413 This server violates the RFC2060 requirement that a BODY[TEXT] fetch should
1414 set the messages's Seen flag.  As a result, if you use the keep option the
1415 same messages will be downloaded over and over.
1416
1417 """
1418             if string.find(greetline, "InterChange") > 0:
1419                 warnings = warnings + """
1420
1421 The InterChange IMAP server at release levels below 3.61.08 screws up
1422 on mail with attachments.  It doesn't fetch them if you give it a
1423 BODY[TEXT] request, though it does if you request RFC822.TEXT.
1424 According to the IMAP RFCs and their maintainer these should be
1425 equivalent -- and we can't drop the BODY[TEXT] form because M$
1426 Exchange (quite legally under RFC2062) rejectsit.  The InterChange
1427 folks claim to have fixed this bug in 3.61.08.
1428
1429 """
1430             if string.find(greetline, "Imail") > 0:
1431                 warnings = warnings + """
1432 We've seen a bug report indicating that this IMAP server (at least as of
1433 version 5.0.7) returns an invalid body size for messages with MIME
1434 attachments; the effect is to drop the attachments on the floor.  We
1435 recommend you upgrade to a non-broken IMAP server.
1436
1437 """
1438             if string.find(greetline, "Domino IMAP4") > 0:
1439                 warnings = warnings + """
1440 Your IMAP server appears to be Lotus Domino.  This server, at least up
1441 to version 4.6.2a, has a bug in its generation of MIME boundaries (see
1442 the details in the fetchmail FAQ).  As a result, even MIME aware MUAs
1443 will see attachments as part of the message text.  If your Domino server's
1444 POP3 facility is enabled, we recommend you fall back on it.
1445
1446 """
1447
1448 ### Checks for protocol variants start here
1449
1450             closebrak = string.find(greetline, ">")
1451             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1452                 warnings = warnings + """
1453 It looks like you could use APOP on this server and avoid sending it your
1454 password in clear.  You should talk to the mailserver administrator about
1455 this.
1456
1457 """
1458             if string.find(greetline, "IMAP2bis") > 0:
1459                 warnings = warnings + """
1460 IMAP2bis servers have a minor problem; they can't peek at messages without
1461 marking them seen.  If you take a line hit during the retrieval, the 
1462 interrupted message may get left on the server, marked seen.
1463
1464 To work around this, it is recommended that you set the `fetchall'
1465 option on all user entries associated with this server, so any stuck
1466 mail will be retrieved next time around.
1467
1468 To fix this bug, upgrade to an IMAP4 server.  The fetchmail FAQ includes
1469 a pointer to an open-source implementation.
1470
1471 """
1472             if string.find(greetline, "IMAP4rev1") > 0:
1473                 warnings = warnings + """
1474 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1475 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1476 has therefore been extremely well tested with this class of server.
1477
1478 """
1479             if warnings == '':
1480                 warnings = warnings + """
1481 Fetchmail doesn't know anything special about this server type.
1482
1483 """
1484
1485             # Display success window with warnings
1486             title = "Autoprobe of " + realhost + " succeeded"
1487             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1488             self.protocol.set(protocol)
1489         confwin.title(title) 
1490         confwin.iconname(title)
1491         Label(confwin, text=title).pack()
1492         Message(confwin, text=confirm, width=600).pack()
1493         Button(confwin, text='Done', 
1494                    command=lambda x=confwin: x.destroy(), bd=2).pack()
1495         
1496 #
1497 # User editing stuff
1498 #
1499
1500 userhelp = {
1501     'title' : 'User option help',
1502     'banner': 'User options',
1503     'text' : """
1504 You may use this panel to set options
1505 that may differ between individual
1506 users on your site.
1507
1508 Once you have a user configuration set
1509 up as you like it, you can select `OK' to
1510 store it in the user list maintained in
1511 the site configuration window.
1512
1513 If you wish to discard the changes you have
1514 made to user options, select `Quit'.
1515 """}
1516
1517 localhelp = {
1518     'title' : 'Local name help',
1519     'banner': 'Local names',
1520     'text' : """
1521 The local name(s) in a user entry are the
1522 people on the client machine who should
1523 receive mail from the poll described.
1524
1525 Note: if a user entry has more than one
1526 local name, messages will be retrieved
1527 in multidrop mode.  This complicates
1528 the configuration issues; see the manual
1529 page section on multidrop mode.
1530
1531 Warning: Be careful with local names
1532 such as foo@bar.com, as that can cause
1533 the mail to be sent to foo@bar.com instead
1534 of sending it to your local system.
1535 """}
1536
1537 class UserEdit(Frame, MyWidget):
1538     def __init__(self, username, parent):
1539         self.parent = parent
1540         self.user = None
1541         for user in parent.server.users:
1542             if user.remote == username:
1543                 self.user = user
1544         if self.user == None:
1545             self.user = User()
1546             self.user.remote = username
1547             self.user.localnames = [username]
1548             parent.server.users.append(self.user)
1549
1550     def edit(self, mode, master=None):
1551         Frame.__init__(self, master)
1552         Pack.config(self)
1553         self.master.title('Fetchmail user ' + self.user.remote
1554                           + ' querying ' + self.parent.server.pollname);
1555         self.master.iconname('Fetchmail user ' + self.user.remote);
1556         self.post(User, 'user')
1557         self.makeWidgets(mode, self.parent.server.pollname)
1558         self.keepalive = []     # Use this to anchor the PhotoImage object
1559         make_icon_window(self, fetchmail_icon)
1560 #       self.grab_set()
1561 #       self.focus_set()
1562 #       self.wait_window()
1563         return self
1564
1565     def destruct(self):
1566         # Yes, this test can fail -- if you delete the parent window.
1567         if self.parent.subwidgets.has_key(self.user.remote):
1568             del self.parent.subwidgets[self.user.remote]
1569         self.master.destroy()
1570
1571     def nosave(self):
1572         if ConfirmQuit(self, 'user option editing'):
1573             self.destruct()
1574
1575     def save(self):
1576         ok = 0
1577         for x in self.user.localnames: ok = ok + (string.find(x, '@') != -1)
1578         if ok == 0 or  Dialog(self, 
1579             title = "Really accept an embedded '@' ?",
1580             text = "Local names with an embedded '@', such as in foo@bar " 
1581                    "might result in your mail being sent to foo@bar.com "
1582                    "instead of your local system.\n Are you sure you want "
1583                    "a local user name with an '@' in it?",
1584             bitmap = 'question',
1585             strings = ('Yes', 'No'),
1586             default = 1).num == 0:
1587                 self.fetch(User, 'user')
1588                 self.destruct()
1589
1590     def makeWidgets(self, mode, servername):
1591         dispose_window(self,
1592                         "User options for " + self.user.remote + " querying " + servername,
1593                         userhelp)
1594
1595         if mode != 'novice':
1596             leftwin = Frame(self);
1597         else:
1598             leftwin = self
1599
1600         secwin = Frame(leftwin, relief=RAISED, bd=5)
1601         Label(secwin, text="Authentication").pack(side=TOP)
1602         LabeledEntry(secwin, 'Password:',
1603                       self.password, '12').pack(side=TOP, fill=X)
1604         secwin.pack(fill=X, anchor=N)
1605
1606         if 'ssl' in feature_options or 'ssl' in dictmembers:
1607             sslwin = Frame(leftwin, relief=RAISED, bd=5)
1608             Checkbutton(sslwin, text="Use SSL?",
1609                         variable=self.ssl).pack(side=TOP, fill=X)
1610             LabeledEntry(sslwin, 'SSL key:',
1611                          self.sslkey, '14').pack(side=TOP, fill=X)
1612             LabeledEntry(sslwin, 'SSL certificate:',
1613                          self.sslcert, '14').pack(side=TOP, fill=X)
1614             Checkbutton(sslwin, text="Check server SSL certificate?",
1615                         variable=self.sslcertck).pack(side=TOP, fill=X)
1616             LabeledEntry(sslwin, 'SSL trusted certificate directory:',
1617                          self.sslcertpath, '14').pack(side=TOP, fill=X)
1618             LabeledEntry(sslwin, 'SSL key fingerprint:',
1619                          self.sslfingerprint, '14').pack(side=TOP, fill=X)
1620             sslwin.pack(fill=X, anchor=N)
1621
1622         names = Frame(leftwin, relief=RAISED, bd=5)
1623         Label(names, text="Local names").pack(side=TOP)
1624         ListEdit("New name: ",
1625                      self.user.localnames, None, None, names, localhelp)
1626         names.pack(fill=X, anchor=N)
1627
1628         if mode != 'novice':
1629             targwin = Frame(leftwin, relief=RAISED, bd=5)
1630             Label(targwin, text="Forwarding Options").pack(side=TOP)
1631             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1632             ListEdit("New listener:",
1633                      self.user.smtphunt, None, None, targwin, None)
1634             Label(targwin, text="Domains to fetch from (ODMR/ETRN only)").pack(side=TOP)
1635             ListEdit("Domains:",
1636                      self.user.fetchdomains, None, None, targwin, None)
1637             LabeledEntry(targwin, 'Append to MAIL FROM line:',
1638                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1639             LabeledEntry(targwin, 'Set RCPT To address:',
1640                      self.smtpname, '26').pack(side=TOP, fill=X)
1641             LabeledEntry(targwin, 'Connection setup command:',
1642                      self.preconnect, '26').pack(side=TOP, fill=X)
1643             LabeledEntry(targwin, 'Connection wrapup command:',
1644                      self.postconnect, '26').pack(side=TOP, fill=X)
1645             LabeledEntry(targwin, 'Local delivery agent:',
1646                      self.mda, '26').pack(side=TOP, fill=X)
1647             LabeledEntry(targwin, 'BSMTP output file:',
1648                      self.bsmtp, '26').pack(side=TOP, fill=X)
1649             LabeledEntry(targwin, 'Listener spam-block codes:',
1650                      self.antispam, '26').pack(side=TOP, fill=X)
1651             LabeledEntry(targwin, 'Pass-through properties:',
1652                      self.properties, '26').pack(side=TOP, fill=X)
1653             Checkbutton(targwin, text="Use LMTP?",
1654                         variable=self.lmtp).pack(side=TOP, fill=X)
1655             targwin.pack(fill=X, anchor=N)
1656
1657         if mode != 'novice':
1658             leftwin.pack(side=LEFT, fill=X, anchor=N)
1659             rightwin = Frame(self)
1660         else:
1661             rightwin = self
1662
1663         optwin = Frame(rightwin, relief=RAISED, bd=5)
1664         Label(optwin, text="Processing Options").pack(side=TOP)
1665         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1666                     variable=self.keep).pack(side=TOP, anchor=W)
1667         Checkbutton(optwin, text="Fetch old messages as well as new",
1668                     variable=self.fetchall).pack(side=TOP, anchor=W)
1669         if mode != 'novice':
1670             Checkbutton(optwin, text="Flush seen messages before retrieval", 
1671                     variable=self.flush).pack(side=TOP, anchor=W)
1672             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply", 
1673                     variable=self.rewrite).pack(side=TOP, anchor=W)
1674             Checkbutton(optwin, text="Force CR/LF at end of each line",
1675                     variable=self.forcecr).pack(side=TOP, anchor=W)
1676             Checkbutton(optwin, text="Strip CR from end of each line",
1677                     variable=self.stripcr).pack(side=TOP, anchor=W)
1678             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1679                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1680             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1681                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1682             Checkbutton(optwin, text="Drop Status lines from forwarded messages", 
1683                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1684             Checkbutton(optwin, text="Drop Delivered-To lines from forwarded messages", 
1685                     variable=self.dropdelivered).pack(side=TOP, anchor=W)
1686         optwin.pack(fill=X)
1687
1688         if mode != 'novice':
1689             limwin = Frame(rightwin, relief=RAISED, bd=5)
1690             Label(limwin, text="Resource Limits").pack(side=TOP)
1691             LabeledEntry(limwin, 'Message size limit:',
1692                       self.limit, '30').pack(side=TOP, fill=X)
1693             LabeledEntry(limwin, 'Size warning interval:',
1694                       self.warnings, '30').pack(side=TOP, fill=X)
1695             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1696                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1697             LabeledEntry(limwin, 'Max messages to forward per poll:',
1698                       self.batchlimit, '30').pack(side=TOP, fill=X)
1699             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1700                 LabeledEntry(limwin, 'Interval between expunges:',
1701                              self.expunge, '30').pack(side=TOP, fill=X)
1702             Checkbutton(limwin, text="Idle after each poll (IMAP only)", 
1703                     variable=self.idle).pack(side=TOP, anchor=W)
1704             limwin.pack(fill=X)
1705
1706             if self.parent.server.protocol == 'IMAP':
1707                 foldwin = Frame(rightwin, relief=RAISED, bd=5)
1708                 Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1709                 ListEdit("New folder:", self.user.mailboxes,
1710                          None, None, foldwin, None)
1711                 foldwin.pack(fill=X, anchor=N)
1712
1713         if mode != 'novice':
1714             rightwin.pack(side=LEFT)
1715         else:
1716             self.pack()
1717
1718
1719 #
1720 # Top-level window that offers either novice or expert mode
1721 # (but not both at once; it disappears when one is selected).
1722 #
1723
1724 class Configurator(Frame):
1725     def __init__(self, outfile, master, onexit, parent):
1726         Frame.__init__(self, master)
1727         self.outfile = outfile
1728         self.onexit = onexit
1729         self.parent = parent
1730         self.master.title('fetchmail configurator');
1731         self.master.iconname('fetchmail configurator');
1732         Pack.config(self)
1733         self.keepalive = []     # Use this to anchor the PhotoImage object
1734         make_icon_window(self, fetchmail_icon)
1735
1736         Message(self, text="""
1737 Use `Novice Configuration' for basic fetchmail setup;
1738 with this, you can easily set up a single-drop connection
1739 to one remote mail server.
1740 """, width=600).pack(side=TOP)
1741         Button(self, text='Novice Configuration',
1742                                 fg='blue', command=self.novice).pack()
1743
1744         Message(self, text="""
1745 Use `Expert Configuration' for advanced fetchmail setup,
1746 including multiple-site or multidrop connections.
1747 """, width=600).pack(side=TOP)
1748         Button(self, text='Expert Configuration',
1749                                 fg='blue', command=self.expert).pack()
1750
1751         Message(self, text="""
1752 Or you can just select `Quit' to leave the configurator now and
1753 return to the main panel.
1754 """, width=600).pack(side=TOP)
1755         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1756         master.protocol("WM_DELETE_WINDOW", self.leave)
1757
1758     def novice(self):
1759         self.master.destroy()
1760         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('novice')
1761
1762     def expert(self):
1763         self.master.destroy()
1764         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('expert')
1765
1766     def leave(self):
1767         self.master.destroy()
1768         self.onexit()
1769
1770 # Run a command in a scrolling text widget, displaying its output
1771
1772 class RunWindow(Frame):
1773     def __init__(self, command, master, parent):
1774         Frame.__init__(self, master)
1775         self.master = master
1776         self.master.title('fetchmail run window');
1777         self.master.iconname('fetchmail run window');
1778         Pack.config(self)
1779         Label(self,
1780                 text="Running "+command, 
1781                 bd=2).pack(side=TOP, pady=10)
1782         self.keepalive = []     # Use this to anchor the PhotoImage object
1783         make_icon_window(self, fetchmail_icon)
1784
1785         # This is a scrolling text window
1786         textframe = Frame(self)
1787         scroll = Scrollbar(textframe)
1788         self.textwidget = Text(textframe, setgrid=TRUE)
1789         textframe.pack(side=TOP, expand=YES, fill=BOTH)
1790         self.textwidget.config(yscrollcommand=scroll.set)
1791         self.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
1792         scroll.config(command=self.textwidget.yview)
1793         scroll.pack(side=RIGHT, fill=BOTH)
1794         textframe.pack(side=TOP)
1795
1796         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1797
1798         self.update()   # Draw widget before executing fetchmail
1799
1800         # Always look for a runnable command in the directory we're running in
1801         # first. This avoids some obscure version-skew errors that can occur
1802         # if you pick up an old fetchmail from the standard system locations.
1803         os.environ["PATH"] = os.path.dirname(sys.argv[0]) + ":" + os.environ["PATH"]
1804         child_stdout = os.popen(command + " 2>&1", "r")
1805         while 1:
1806             ch = child_stdout.read(1)
1807             if not ch:
1808                 break
1809             self.textwidget.insert(END, ch)
1810         self.textwidget.insert(END, "Done.")
1811         self.textwidget.see(END);
1812
1813     def leave(self):
1814         self.master.destroy()
1815
1816 # Here's where we choose either configuration or launching
1817
1818 class MainWindow(Frame):
1819     def __init__(self, outfile, master=None):
1820         Frame.__init__(self, master)
1821         self.outfile = outfile
1822         self.master.title('fetchmail launcher');
1823         self.master.iconname('fetchmail launcher');
1824         Pack.config(self)
1825         Label(self,
1826                 text='Fetchmailconf ' + version, 
1827                 bd=2).pack(side=TOP, pady=10)
1828         self.keepalive = []     # Use this to anchor the PhotoImage object
1829         make_icon_window(self, fetchmail_icon)
1830         self.debug = 0
1831
1832         ## Test icon display with the following:
1833         # icon_image = PhotoImage(data=fetchmail_icon)
1834         # Label(self, image=icon_image).pack(side=TOP, pady=10)
1835         # self.keepalive.append(icon_image)
1836
1837         Message(self, text="""
1838 Use `Configure fetchmail' to tell fetchmail about the remote
1839 servers it should poll (the host name, your username there,
1840 whether to use POP or IMAP, and so forth).
1841 """, width=600).pack(side=TOP)
1842         self.configbutton = Button(self, text='Configure fetchmail',
1843                                 fg='blue', command=self.configure)
1844         self.configbutton.pack()
1845
1846         Message(self, text="""
1847 Use `Run fetchmail' to run fetchmail with debugging enabled.
1848 This is a good way to test out a new configuration.
1849 """, width=600).pack(side=TOP)
1850         Button(self, text='Run fetchmail',fg='blue', command=self.test).pack()
1851
1852         Message(self, text="""
1853 Use `Run fetchmail' to run fetchmail in foreground.
1854 Progress  messages will be shown, but not debug messages.
1855 """, width=600).pack(side=TOP)
1856         Button(self, text='Run fetchmail', fg='blue', command=self.run).pack()
1857
1858         Message(self, text="""
1859 Or you can just select `Quit' to exit the launcher now.
1860 """, width=600).pack(side=TOP)
1861         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1862
1863     def configure(self):
1864         self.configbutton.configure(state=DISABLED)
1865         Configurator(self.outfile, Toplevel(),
1866                      lambda self=self: self.configbutton.configure(state=NORMAL),
1867                      self)
1868     def test(self):
1869         RunWindow("fetchmail -d0 -v --nosyslog", Toplevel(), self)
1870
1871     def run(self):
1872         RunWindow("fetchmail -d0", Toplevel(), self)
1873
1874     def leave(self):
1875         self.quit()
1876
1877 # Functions for turning a dictionary into an instantiated object tree.
1878
1879 def intersect(list1, list2):
1880 # Compute set intersection of lists
1881     res = []
1882     for x in list1:
1883         if x in list2:
1884             res.append(x)
1885     return res
1886
1887 def setdiff(list1, list2):
1888 # Compute set difference of lists
1889     res = []
1890     for x in list1:
1891         if not x in list2:
1892             res.append(x)
1893     return res
1894
1895 def copy_instance(toclass, fromdict):
1896 # Initialize a class object of given type from a conformant dictionary.
1897     for fld in fromdict.keys():
1898         if not fld in dictmembers:
1899             dictmembers.append(fld)
1900 # The `optional' fields are the ones we can ignore for purposes of
1901 # conformability checking; they'll still get copied if they are
1902 # present in the dictionary.
1903     optional = ('interface', 'monitor',
1904                 'netsec', 'esmtpname', 'esmtppassword',
1905                 'ssl', 'sslkey', 'sslcert', 'sslproto', 'sslcertck',
1906                 'sslcertpath', 'sslfingerprint', 'showdots')
1907     class_sig = setdiff(toclass.__dict__.keys(), optional)
1908     class_sig.sort()
1909     dict_keys = setdiff(fromdict.keys(), optional)
1910     dict_keys.sort()
1911     common = intersect(class_sig, dict_keys)
1912     if 'typemap' in class_sig: 
1913         class_sig.remove('typemap')
1914     if tuple(class_sig) != tuple(dict_keys):
1915         print "Fields don't match what fetchmailconf expected:"
1916 #       print "Class signature: " + `class_sig`
1917 #       print "Dictionary keys: " + `dict_keys`
1918         diff = setdiff(class_sig, common)
1919         if diff:
1920             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
1921         diff = setdiff(dict_keys, common)
1922         if diff:
1923             print "Not matched in dictionary keys: " + `diff`
1924         sys.exit(1)
1925     else:
1926         for x in fromdict.keys():
1927             setattr(toclass, x, fromdict[x])
1928
1929 #
1930 # And this is the main sequence.  How it works:  
1931 #
1932 # First, call `fetchmail --configdump' and trap the output in a tempfile.
1933 # This should fill it with a Python initializer for a variable `fetchmailrc'.
1934 # Run execfile on the file to pull fetchmailrc into Python global space.
1935 # You don't want static data, though; you want, instead, a tree of objects
1936 # with the same data members and added appropriate methods.
1937 #
1938 # This is what the copy_instance function() is for.  It tries to copy a
1939 # dictionary field by field into a class, aborting if the class and dictionary
1940 # have different data members (except for any typemap member in the class;
1941 # that one is strictly for use by the MyWidget supperclass).
1942 #
1943 # Once the object tree is set up, require user to choose novice or expert
1944 # mode and instantiate an edit object for the configuration.  Class methods
1945 # will take it all from there.
1946 #
1947 # Options (not documented because they're for fetchmailconf debuggers only):
1948 # -d: Read the configuration and dump it to stdout before editing.  Dump
1949 #     the edited result to stdout as well.
1950 # -f: specify the run control file to read.
1951
1952 if __name__ == '__main__': 
1953
1954     if not os.environ.has_key("DISPLAY"):
1955         print "fetchmailconf must be run under X"
1956         sys.exit(1)
1957
1958     fetchmail_icon = """
1959 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
1960 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
1961 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
1962 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
1963 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
1964 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
1965 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
1966 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
1967 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
1968 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
1969 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
1970 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
1971 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
1972 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
1973 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
1974 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
1975 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
1976 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
1977 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
1978 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
1979 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
1980 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
1981 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
1982 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
1983 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
1984 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
1985 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
1986 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
1987 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
1988 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
1989 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
1990 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
1991 """
1992 # The base64 data in the string above was generated by the following procedure:
1993 #
1994 # import base64
1995 # print base64.encodestring(open("fetchmail.gif", "rb").read())
1996 #
1997
1998     # Process options
1999     (options, arguments) = getopt.getopt(sys.argv[1:], "df:")
2000     dump = rcfile = None;
2001     for (switch, val) in options:
2002         if (switch == '-d'):
2003             dump = TRUE
2004         elif (switch == '-f'):
2005             rcfile = val
2006
2007     # Get client host's FQDN
2008     hostname = socket.gethostbyaddr(socket.gethostname())[0]
2009
2010     # Compute defaults
2011     ConfigurationDefaults = Configuration()
2012     ServerDefaults = Server()
2013     UserDefaults = User()
2014
2015     # Read the existing configuration.  We set the umask to 077 to make sure
2016     # that group & other read/write permissions are shut off -- we wouldn't
2017     # want crackers to snoop password information out of the tempfile.
2018     tmpfile = tempfile.mktemp()
2019     if rcfile:
2020         cmd = "umask 077; fetchmail -f " + rcfile + " --configdump --nosyslog >" + tmpfile
2021     else:
2022         cmd = "umask 077; fetchmail --configdump --nosyslog >" + tmpfile
2023         
2024     try:
2025         s = os.system(cmd)
2026         if s != 0:
2027             print "`" + cmd + "' run failure, status " + `s`
2028             raise SystemExit
2029     except:
2030         print "Unknown error while running fetchmail --configdump"
2031         os.remove(tmpfile)
2032         sys.exit(1)
2033
2034     try:
2035         execfile(tmpfile)
2036     except:
2037         print "Can't read configuration output of fetchmail --configdump."
2038         os.remove(tmpfile)
2039         sys.exit(1)
2040         
2041     os.remove(tmpfile)
2042
2043     # The tricky part -- initializing objects from the configuration global
2044     # `Configuration' is the top level of the object tree we're going to mung.
2045     # The dictmembers list is used to track the set of fields the dictionary
2046     # contains; in particular, we can use it to tell whether things like the
2047     # monitor, interface, netsec, ssl, sslkey, or sslcert fields are present.
2048     dictmembers = []
2049     Fetchmailrc = Configuration()
2050     copy_instance(Fetchmailrc, fetchmailrc)
2051     Fetchmailrc.servers = [];
2052     for server in fetchmailrc['servers']:
2053         Newsite = Server()
2054         copy_instance(Newsite, server)
2055         Fetchmailrc.servers.append(Newsite)
2056         Newsite.users = [];
2057         for user in server['users']:
2058             Newuser = User()
2059             copy_instance(Newuser, user)
2060             Newsite.users.append(Newuser)
2061
2062     # We may want to display the configuration and quit
2063     if dump:
2064         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
2065
2066     # The theory here is that -f alone sets the rcfile location,
2067     # but -d and -f together mean the new configuration should go to stdout.
2068     if not rcfile and not dump:
2069         rcfile = os.environ["HOME"] + "/.fetchmailrc"
2070
2071     # OK, now run the configuration edit
2072     root = MainWindow(rcfile)
2073     root.mainloop()
2074
2075 # The following sets edit modes for GNU EMACS
2076 # Local Variables:
2077 # mode:python
2078 # End: