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