]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf
Clean up the GIF initializer syntax.
[~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 #
8 # TO DO: Arrange for save and quit buttons to clean up all frames dependent
9 # on the current ones.
10 version = "1.2"
11
12 from Tkinter import *
13 from Dialog import *
14 import sys, time, os, string, socket, getopt
15
16 #
17 # Define the data structures the GUIs will be tossing around
18 #
19 class Configuration:
20     def __init__(self):
21         self.poll_interval = 0          # Normally, run in foreground
22         self.syslog = FALSE             # Use syslogd for logging?
23         self.logfile = None             # No logfile, initially
24         self.idfile = os.environ["HOME"] + "/.fetchids"         # Default idfile, initially
25         self.postmaster = None          # No last-resort address, initially
26         self.invisible = FALSE          # Suppress Received line & spoof?
27         self.servers = []               # List of included sites
28         Configuration.typemap = (
29             ('poll_interval',   'Int'),
30             ('syslog',          'Boolean'),
31             ('logfile',         'String'),
32             ('idfile',          'String'),
33             ('postmaster',      'String'),
34             ('invisible',       'Boolean'))
35
36     def __repr__(self):
37         str = "";
38         if self.syslog != ConfigurationDefaults.syslog:
39            str = str + ("set syslog\n")
40         elif self.logfile:
41             str = str + ("set logfile \"%s\"\n" % (self.logfile,));
42         if self.idfile != ConfigurationDefaults.idfile:
43             str = str + ("set idfile \"%s\"\n" % (self.idfile,));
44         if self.postmaster != ConfigurationDefaults.postmaster:
45             str = str + ("set postmaster \"%s\"\n" % (self.postmaster,));
46         if self.poll_interval > 0:
47             str = str + "set daemon " + `self.poll_interval` + "\n"
48         for site in self.servers:
49             str = str + repr(site)
50         return str
51
52     def __str__(self):
53         return "[Configuration: " + repr(self) + "]"
54
55 class Server:
56     def __init__(self):
57         self.pollname = None            # Poll label
58         self.via = None                 # True name of host
59         self.active = TRUE              # Poll status
60         self.interval = 0               # Skip interval
61         self.protocol = 'auto'          # Default to auto protocol
62         self.port = 0                   # Port number to use
63         self.uidl = FALSE               # Don't use RFC1725 UIDLs by default
64         self.auth = 'password'          # Default to password authentication
65         self.timeout = 300              # 5-minute timeout
66         self.envelope = 'Received'      # Envelope-address header
67         self.envskip = 0                # Number of envelope headers to skip
68         self.qvirtual = None            # Name prefix to strip
69         self.aka = []                   # List of DNS aka names
70         self.dns = TRUE                 # Enable DNS lookup on multidrop
71         self.localdomains = []          # Domains to be considered local
72         self.interface = None           # IP address and range
73         self.monitor = None             # IP address and range
74         self.netsec = None              # IPV6 security options
75         self.users = []                 # List of user entries for site
76         Server.typemap = (
77             ('pollname',  'String'),
78             ('via',       'String'),
79             ('active',    'Boolean'),
80             ('interval',  'Int'),
81             ('protocol',  'String'),
82             ('interval',  'Int'),
83             ('port',      'Int'),
84             ('uidl',      'Boolean'),
85             ('auth',      'String'),
86             ('timeout',   'Int'),
87             ('envelope',  'String'),
88             ('envskip',   'Int'),
89             ('qvirtual',  'String'),
90             # leave aka out
91             ('dns',       'Boolean'),
92             # leave localdomains out
93             ('interface', 'String'),
94             ('monitor',   'String'),
95             ('netsec',   'String'))
96
97     def dump(self, folded):
98         str = ""
99         if self.active:   str = str + "poll"
100         else:             str = str + "skip"
101         str = str + (" " + self.pollname)
102         if self.via:
103             str = str + (" via \"%s\"\n" % (self.via,));
104         if self.protocol != ServerDefaults.protocol:
105             str = str + " with proto " + self.protocol 
106         if self.port != defaultports[self.protocol] and self.port != 0:
107             str = str + " port " + `self.port`
108         if self.timeout != ServerDefaults.timeout:
109             str = str + " timeout " + `self.timeout`
110         if self.interval != ServerDefaults.interval: 
111             str = str + " interval " + `self.interval`
112         if self.envelope != ServerDefaults.envelope or self.envskip != ServerDefaults.envskip:
113             if self.envskip:
114                 str = str + " envelope " + self.envskip + " " + self.envelope
115             else:
116                 str = str + " envelope " + self.envelope
117         if self.qvirtual:
118             str = str + (" qvirtual \"%s\"\n" % (self.qvirtual,));
119         if self.auth != ServerDefaults.auth:
120             str = str + " auth " + self.auth
121         if self.dns != ServerDefaults.dns or self.uidl != ServerDefaults.uidl:
122             str = str + " and options"
123         if self.dns != ServerDefaults.dns:
124             str = str + flag2str(self.dns, 'dns')
125         if self.uidl != ServerDefaults.uidl:
126             str = str + flag2str(self.uidl, 'uidl')
127         if folded:        str = str + "\n    "
128         else:             str = str + " "
129
130         if self.aka:
131              str = str + "aka"
132              for x in self.aka:
133                 str = str + " " + x
134         if self.aka and self.localdomains: str = str + " "
135         if self.localdomains:
136              str = str + ("localdomains")
137              for x in self.localdomains:
138                 str = str + " " + x
139         if (self.aka or self.localdomains):
140             if folded:
141                 str = str + "\n    "
142             else:
143                 str = str + " "
144
145         if self.interface:
146             str = str + "interface \"" + self.interface + "\""
147         if self.monitor:
148             str = str + "monitor \"" + self.monitor + "\""
149         if self.netsec:
150             str = str + "netsec \"" + self.netsec + "\""
151         if self.interface or self.monitor or self.netsec:
152             if folded:
153                 str = str + "\n"
154
155         if str[-1] == " ": str = str[0:-1]
156
157         for user in self.users:
158             str = str + repr(user)
159         str = str + "\n"
160         return str;
161
162     def __repr__(self):
163         return self.dump(TRUE)
164
165     def __str__(self):
166         return "[Server: " + self.dump(FALSE) + "]"
167
168 class User:
169     def __init__(self):
170         self.remote = os.environ["USER"]# Remote username
171         self.localnames = [self.remote,]# Local names
172         self.password = None            # Password for mail account access
173         self.mailboxes = []             # Remote folders to retrieve from
174         self.smtphunt = []              # Hosts to forward to
175         self.smtpaddress = None;        # Append this to MAIL FROM line
176         self.preconnect = None          # Connection setup
177         self.postconnect = None         # Connection wrapup
178         self.mda = None                 # Mail Delivery Agent
179         self.antispam = "571 550 501"   # Listener's spam-block code
180         self.keep = FALSE               # Keep messages
181         self.flush = FALSE              # Flush messages
182         self.fetchall = FALSE           # Fetch old messages
183         self.rewrite = TRUE             # Rewrite message headers
184         self.forcecr = FALSE            # Force LF -> CR/LF
185         self.stripcr = FALSE            # Strip CR
186         self.pass8bits = FALSE          # Force BODY=7BIT
187         self.mimedecode = FALSE         # Undo MIME armoring
188         self.dropstatus = FALSE         # Drop incoming Status lines
189         self.limit = 0                  # Message size limit
190         self.fetchlimit = 0             # Max messages fetched per batch
191         self.batchlimit = 0             # Max message forwarded per batch
192         self.expunge = 1                # Interval between expunges (IMAP)
193         User.typemap = (
194             ('remote',      'String'),
195             # leave out mailboxes and localnames
196             ('password',    'String'),
197             # Leave out smtphunt
198             ('smtpaddress', 'String'),
199             ('preconnect',  'String'),
200             ('postconnect', 'String'),
201             ('mda',         'String'),
202             ('antispam',    'String'),
203             ('keep',        'Boolean'),
204             ('flush',       'Boolean'),
205             ('fetchall',    'Boolean'),
206             ('rewrite',     'Boolean'),
207             ('forcecr',     'Boolean'),
208             ('stripcr',     'Boolean'),
209             ('pass8bits',   'Boolean'),
210             ('mimedecode',  'Boolean'),
211             ('dropstatus',  'Boolean'),
212             ('limit',       'Int'),
213             ('fetchlimit',  'Int'),
214             ('batchlimit',  'Int'),
215             ('expunge',     'Int'))
216
217     def __repr__(self):
218         str = "    "
219         str = str + "user \"" + self.remote + "\" there ";
220         if self.password:
221             str = str + "with password \"" + self.password + '" '
222         if self.localnames:
223             str = str + "is"
224             for x in self.localnames:
225                 str = str + " " + x
226             str = str + " here"
227         if (self.keep != UserDefaults.keep
228                 or self.flush != UserDefaults.flush
229                 or self.fetchall != UserDefaults.fetchall
230                 or self.rewrite != UserDefaults.rewrite 
231                 or self.forcecr != UserDefaults.forcecr 
232                 or self.stripcr != UserDefaults.stripcr 
233                 or self.pass8bits != UserDefaults.pass8bits
234                 or self.mimedecode != UserDefaults.mimedecode
235                 or self.dropstatus != UserDefaults.dropstatus):
236             str = str + " options"
237         if self.keep != UserDefaults.keep:
238             str = str + flag2str(self.keep, 'keep')
239         if self.flush != UserDefaults.flush:
240             str = str + flag2str(self.flush, 'flush')
241         if self.fetchall != UserDefaults.fetchall:
242             str = str + flag2str(self.fetchall, 'fetchall')
243         if self.rewrite != UserDefaults.rewrite:
244             str = str + flag2str(self.rewrite, 'rewrite')
245         if self.forcecr != UserDefaults.forcecr:
246             str = str + flag2str(self.forcecr, 'forcecr')
247         if self.stripcr != UserDefaults.stripcr:
248             str = str + flag2str(self.stripcr, 'stripcr')
249         if self.pass8bits != UserDefaults.pass8bits:
250             str = str + flag2str(self.pass8bits, 'pass8bits')
251         if self.mimedecode != UserDefaults.mimedecode:
252             str = str + flag2str(self.mimedecode, 'mimedecode')
253         if self.dropstatus != UserDefaults.dropstatus:
254             str = str + flag2str(self.dropstatus, 'dropstatus')
255         if self.limit != UserDefaults.limit:
256             str = str + " limit " + `self.limit`
257         if self.fetchlimit != UserDefaults.fetchlimit:
258             str = str + " fetchlimit " + `self.fetchlimit`
259         if self.batchlimit != UserDefaults.batchlimit:
260             str = str + " batchlimit " + `self.batchlimit`
261         if self.expunge != UserDefaults.expunge:
262             str = str + " expunge " + `self.expunge`
263         str = str + "\n"
264         trimmed = self.smtphunt;
265         if trimmed != [] and trimmed[len(trimmed) - 1] == "localhost":
266             trimmed = trimmed[0:len(trimmed) - 1]
267         if trimmed != [] and trimmed[len(trimmed) - 1] == hostname:
268             trimmed = trimmed[0:len(trimmed) - 1]
269         if trimmed != []:
270             str = str + "    smtphost "
271             for x in trimmed:
272                 str = str + " " + x
273                 str = str + "\n"
274         if self.mailboxes:
275              str = str + "    folder"
276              for x in self.mailboxes:
277                 str = str + " " + x
278              str = str + "\n"
279         for fld in ('smtpaddress', 'preconnect', 'postconnect', 'mda'):
280             if getattr(self, fld):
281                 str = str + " %s \"%s\"\n" % (fld, `getattr(self, fld)`)
282         if self.antispam != UserDefaults.antispam:
283             str = str + "    antispam " + self.antispam + "\n"
284         return str;
285
286     def __str__(self):
287         return "[User: " + repr(self) + "]"
288
289 #
290 # Helper code
291 #
292
293 defaultports = {"auto":0,
294                 "POP2":109, 
295                 "POP3":110,
296                 "APOP":110,
297                 "KPOP":1109,
298                 "IMAP":143,
299                 "IMAP-K4":143,
300                 "ETRN":25}
301
302 protolist = ("auto", "POP2", "POP3", "APOP", "KPOP", "IMAP", "IMAP-K4", "ETRN")
303
304 authlist = ("password", "kerberos")
305
306 listboxhelp = {
307     'title' : 'List Selection Help',
308     'banner': 'List Selection',
309     'text' : """
310 You must select an item in the list box (by clicking on it). 
311 """}
312
313 def flag2str(value, string):
314 # make a string representation of a .fetchmailrc flag or negated flag
315     str = ""
316     if value != None:
317         str = str + (" ")
318         if value == FALSE: str = str + ("no ")
319         str = str + string;
320     return str
321
322 class LabeledEntry(Frame):
323 # widget consisting of entry field with caption to left
324     def bind(self, key, action):
325         self.E.bind(key, action)
326     def focus_set(self):
327         self.E.focus_set()
328     def __init__(self, Master, text, textvar, lwidth, ewidth=12):
329         Frame.__init__(self, Master)
330         self.L = Label(self, {'text':text, 'width':lwidth, 'anchor':'w'})
331         self.E = Entry(self, {'textvar':textvar, 'width':ewidth})
332         self.L.pack({'side':'left'})
333         self.E.pack({'side':'left', 'expand':'1', 'fill':'x'})
334
335 def ButtonBar(frame, legend, ref, alternatives, depth, command):
336 # array of radio buttons, caption to left, picking from a string list
337     bar = Frame(frame)
338     width = len(alternatives) / depth;
339     Label(bar, text=legend).pack(side=LEFT)
340     for column in range(width):
341         subframe = Frame(bar)
342         for row in range(depth):
343             ind = width * row + column
344             Radiobutton(subframe,
345                         {'text':alternatives[ind], 
346                          'variable':ref,
347                          'value':alternatives[ind],
348                          'command':command}).pack(side=TOP, anchor=W)
349         subframe.pack(side=LEFT)
350     bar.pack(side=TOP);
351     return bar
352
353 def helpwin(helpdict):
354 # help message window with a self-destruct button
355     helpwin = Toplevel()
356     helpwin.title(helpdict['title']) 
357     helpwin.iconname(helpdict['title'])
358     Label(helpwin, text=helpdict['banner']).pack()
359     textwin = Message(helpwin, text=helpdict['text'], width=600)
360     textwin.pack()
361     Button(helpwin, text='Done', 
362            command=lambda x=helpwin: Widget.destroy(x),
363            relief=SUNKEN, bd=2).pack()
364
365 def make_icon_window(base, image):
366     try:
367         # Some older pythons will error out on this
368         icon_image = PhotoImage(data=image)
369         icon_window = Toplevel()
370         Label(icon_window, image=icon_image, bg='black').pack()
371         base.master.iconwindow(icon_window)
372         # Avoid TkInter brain death. PhotoImage objects go out of
373         # scope when the enclosing function returns.  Therefore
374         # we have to explicitly link them to something.
375         base.keepalive.append(icon_image)
376     except:
377         pass
378
379 class ListEdit(Frame):
380 # edit a list of values (duplicates not allowed) with a supplied editor hook 
381     def __init__(self, newlegend, list, editor, master, helptxt):
382         self.editor = editor
383         self.list = list
384
385         # Set up a widget to accept new elements
386         self.newval = StringVar(master)
387         newwin = LabeledEntry(master, newlegend, self.newval, '12')
388         newwin.bind('<Double-1>', self.handleNew)
389         newwin.bind('<Return>', self.handleNew)
390         newwin.pack(side=TOP, fill=X, anchor=E)
391
392         # Edit the existing list
393         listframe = Frame(master)
394         scroll = Scrollbar(listframe)
395         self.listwidget = Listbox(listframe, height=0, selectmode='browse')
396         if self.list:
397             for x in self.list:
398                 self.listwidget.insert(END, x)
399         listframe.pack(side=TOP, expand=YES, fill=BOTH)
400         self.listwidget.config(yscrollcommand=scroll.set, relief=SUNKEN)
401         self.listwidget.pack(side=LEFT, expand=YES, fill=BOTH)
402         scroll.config(command=self.listwidget.yview, relief=SUNKEN)
403         scroll.pack(side=RIGHT, fill=BOTH)
404         self.listwidget.config(selectmode=SINGLE, setgrid=TRUE)
405         self.listwidget.bind('<Double-1>', self.handleList);
406         self.listwidget.bind('<Return>', self.handleList);
407
408         bf = Frame(master);
409         if self.editor:
410             Button(bf, text='Edit',   command=self.editItem).pack(side=LEFT)
411         Button(bf, text='Delete', command=self.deleteItem).pack(side=LEFT)
412         if helptxt:
413             self.helptxt = helptxt
414             Button(bf, text='Help', fg='blue',
415                    command=self.help).pack(side=RIGHT)
416         bf.pack(fill=X)
417
418     def help(self):
419         helpwin(self.helptxt)
420
421     def handleList(self, event):
422         self.editItem();
423
424     def handleNew(self, event):
425         item = self.newval.get()
426         entire = self.listwidget.get(0, self.listwidget.index('end'));
427         if item and (not entire) or (not item in self.listwidget.get(0, self.listwidget.index('end'))):
428             self.listwidget.insert('end', item)
429             if self.list != None: self.list.append(item)
430         self.newval.set('')
431
432     def editItem(self):
433         select = self.listwidget.curselection()
434         if not select:
435             helpwin(listboxhelp)
436         else:
437             index = select[0]
438             if index and self.editor:
439                 label = self.listwidget.get(index);
440                 apply(self.editor, (label,))
441
442     def deleteItem(self):
443         select = self.listwidget.curselection()
444         if not select:
445             helpwin(listboxhelp)
446         else:
447             index = string.atoi(select[0])
448             if index:
449                 self.listwidget.delete(index)
450                 if self.list != None: del self.list[index]
451
452 def ConfirmQuit(frame, context):
453     ans = Dialog(frame, 
454                  title = 'Quit?',
455                  text = 'Really quit ' + context + ' without saving?',
456                  bitmap = 'question',
457                  strings = ('Yes', 'No'),
458                  default = 1)
459     return ans.num == 0
460
461 def dispose_window(master, legend, help):
462     dispose = Frame(master, relief=RAISED, bd=5)
463     Label(dispose, text=legend).pack(side=TOP,pady=10)
464     Button(dispose, text='Save', fg='blue',
465            command=master.save).pack(side=LEFT)
466     Button(dispose, text='Quit', fg='blue',
467            command=master.nosave).pack(side=LEFT)
468     Button(dispose, text='Help', fg='blue',
469            command=lambda x=help: helpwin(x)).pack(side=RIGHT)
470     dispose.pack(fill=X)
471     return dispose
472
473 class MyWidget:
474 # Common methods for Tkinter widgets -- deals with Tkinter declaration
475     def post(self, widgetclass, field):
476         for x in widgetclass.typemap:
477             if x[1] == 'Boolean':
478                 setattr(self, x[0], BooleanVar(self))
479             elif x[1] == 'String':
480                 setattr(self, x[0], StringVar(self))
481             elif x[1] == 'Int':
482                 setattr(self, x[0], IntVar(self))
483             source = getattr(getattr(self, field), x[0])
484             if source:
485                 getattr(self, x[0]).set(source)
486
487     def fetch(self, widgetclass, field):
488         for x in widgetclass.typemap:
489             setattr(getattr(self, field), x[0], getattr(self, x[0]).get())
490
491 #
492 # First, code to set the global fetchmail run controls.
493 #
494
495 configure_novice_help = {
496     'title' : 'Fetchmail novice configurator help',
497     'banner': 'Novice configurator help',
498     'text' : """
499 In the `Novice Configurator Controls' panel, you can:
500
501 Press `Save' to save the new fetchmail configuration you have created.
502 Press `Quit' to exit without saving.
503 Press `Help' to bring up this help message.
504
505 In the `Novice Configuration' panels, you will set up the basic data
506 needed to create a simple fetchmail setup.  These include:
507
508 1. The name of the remote site you want to query.
509
510 2. Your login name on that site.
511
512 3. Your password on that site.
513
514 4. A protocol to use (POP, IMAP, ETRN, etc.)
515
516 5. A polling interval.
517
518 6. Options to fetch old messages as well as new, uor to suppress
519    deletion of fetched message.
520
521 The novice-configuration code will assume that you want to forward mail
522 to a local sendmail listener with no special options.
523 """}
524
525 configure_expert_help = {
526     'title' : 'Fetchmail expert configurator help',
527     'banner': 'Expert configurator help',
528     'text' : """
529 In the `Expert Configurator Controls' panel, you can:
530
531 Press `Save' to save the new fetchmail configuration you have edited.
532 Press `Quit' to exit without saving.
533 Press `Help' to bring up this help message.
534
535 In the `Run Controls' panel, you can set the following options that
536 control how fetchmail runs:
537
538 Poll interval
539         Number of seconds to wait between polls in the background.
540         If zero, fetchmail will run in foreground.
541
542 Logfile
543         If empty, emit progress and error messages to stderr.
544         Otherwise this gives the name of the files to write to.
545         This field is ignored if the "Log to syslog?" option is on.
546
547 Idfile
548         If empty, store seen-message IDs in .fetchids under user's home
549         directory.  If nonempty, use given file name.
550
551 Postmaster
552         Who to send multidrop mail to as a last resort if no address can
553         be matched.  Normally empty; in this case, fetchmail treats the
554         invoking user as the address of last resort unless that user is
555         root.  If that user is root, fetchmail sends to `postmaster'.
556
557 Invisible
558         If false (the default) fetchmail generates a Received line into
559         each message and generates a HELO from the machine it is running on.
560         If true, fetchmail generates no Received line and HELOs as if it were
561         the remote site.
562
563 In the `Remote Mail Configurations' panel, you can:
564
565 1. Enter the name of a new remote mail server you want fetchmail to query.
566
567 To do this, simply enter a label for the poll configuration in the
568 `New Server:' box.  The label should be a DNS name of the server (unless
569 you are using ssh or some other tunneling method and will fill in the `via'
570 option on the site configuration screen).
571
572 2. Change the configuration of an existing site.
573
574 To do this, find the site's label in the listbox and double-click it.
575 This will take you to a site configuration dialogue.
576 """}
577
578
579 class ConfigurationEdit(Frame, MyWidget):
580     def __init__(self, configuration, outfile, master=None):
581         self.configuration = configuration
582         self.outfile = outfile
583         self.container = master
584         ConfigurationEdit.mode_to_help = {
585             'novice':configure_novice_help, 'expert':configure_expert_help
586             }
587
588     def edit(self, mode):
589         self.mode = mode
590         Frame.__init__(self, self.container)
591         self.master.title('fetchmail ' + self.mode + ' configurator');
592         self.master.iconname('fetchmail ' + self.mode + ' configurator');
593         self.keepalive = []     # Use this to anchor the PhotoImage object
594         make_icon_window(self, fetchmail_gif)
595         Pack.config(self)
596         self.post(Configuration, 'configuration')
597
598         dispose_window(self,
599                        'Configurator ' + self.mode + ' Controls',
600                        ConfigurationEdit.mode_to_help[self.mode])
601
602         gf = Frame(self, relief=RAISED, bd = 5)
603         Label(gf,
604                 text='Fetchmail Run Controls', 
605                 bd=2).pack(side=TOP, pady=10)
606
607         df = Frame(gf)
608
609         # Set the poll interval
610         de = LabeledEntry(df, '     Poll interval:', self.poll_interval, '14')
611         de.pack(side=RIGHT, anchor=E)
612         df.pack()
613
614         if self.mode != 'novice':
615             sf = Frame(gf)
616             Checkbutton(sf,
617                 {'text':'Log to syslog?',
618                 'variable':self.syslog,
619                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
620             log = LabeledEntry(sf, '     Logfile:', self.logfile, '14')
621             log.pack(side=RIGHT, anchor=E)
622             sf.pack(fill=X)
623
624             Checkbutton(gf,
625                 {'text':'Invisible mode?',
626                 'variable':self.invisible,
627                  'relief':GROOVE}).pack(side=LEFT, anchor=W)
628             # Set the idfile
629             log = LabeledEntry(gf, '     Idfile:', self.idfile, '14')
630             log.pack(side=RIGHT, anchor=E)
631             # Set the postmaster
632             log = LabeledEntry(gf, '     Postmaster:', self.postmaster, '14')
633             log.pack(side=RIGHT, anchor=E)
634
635         gf.pack(fill=X)
636
637         # Expert mode allows us to edit multiple sites
638         lf = Frame(self, relief=RAISED, bd=5)
639         Label(lf,
640               text='Remote Mail Server Configurations', 
641               bd=2).pack(side=TOP, pady=10)
642         ListEdit('New Server:', 
643                 map(lambda x: x.pollname, self.configuration.servers),
644                 lambda site, m=self.mode, s=self.configuration.servers:
645                  ServerEdit(site, s).edit(m, Toplevel()),
646                 lf, remotehelp)
647         lf.pack(fill=X)
648
649     def nosave(self):
650         if ConfirmQuit(self, self.mode + " configuration editor"):
651             self.quit()
652
653     def save(self):
654         self.fetch(Configuration, 'configuration')
655         fm = None
656         if not self.outfile:
657             fm = sys.stdout
658         elif not os.path.isfile(self.outfile) or Dialog(self, 
659                  title = 'Overwrite existing run control file?',
660                  text = 'Really overwrite existing run control file?',
661                  bitmap = 'question',
662                  strings = ('Yes', 'No'),
663                  default = 1).num == 0:
664             fm = open(self.outfile, 'w')
665         if fm:
666             fm.write("# Configuration created %s by fetchmailconf\n" % time.ctime(time.time()))
667             fm.write(`self.configuration`)
668             os.chmod(self.outfile, 0600)
669             self.quit()
670
671 #
672 # Server editing stuff.
673 #
674 remotehelp = {
675     'title' : 'Remote site help',
676     'banner': 'Remote sites',
677     'text' : """
678 When you add a site name to the list here, 
679 you initialize an entry telling fetchmail
680 how to poll a new site.
681
682 When you select a sitename (by double-
683 clicking it, or by single-clicking to
684 select and then clicking the Edit button),
685 you will open a window to configure that
686 site.
687 """}
688
689 serverhelp = {
690     'title' : 'Server options help',
691     'banner': 'Server Options',
692     'text' : """
693 The server options screen controls fetchmail 
694 options that apply to one of your mailservers.
695
696 Once you have a mailserver configuration set
697 up as you like it, you can select `Save' to
698 store it in the server list maintained in
699 the main configuration window.
700
701 If you wish to discard changes to a server 
702 configuration, select `Quit'.
703 """}
704
705 controlhelp = {
706     'title' : 'Run Control help',
707     'banner': 'Run Controls',
708     'text' : """
709 If the `Poll normally' checkbox is on, the host is polled as part of
710 the normal operation of fetchmail when it is run with no arguments.
711 If it is off, fetchmail will only query this host when it is given as
712 a command-line argument.
713
714 The `True name of server' box should specify the actual DNS name
715 to query. By default this is the same as the poll name.
716
717 Normally each host described in the file is queried once each 
718 poll cycle. If `Cycles to skip between polls' is greater than 0,
719 that's the number of poll cycles that are skipped between the
720 times this post is actually polled.
721
722 The `Server timeout' is the number of seconds fetchmail will wait
723 for a reply from the mailserver before concluding it is hung and
724 giving up.
725 """}
726
727 protohelp = {
728     'title' : 'Protocol and Port help',
729     'banner': 'Protocol and Port',
730     'text' : """
731 These options control the remote-mail protocol
732 and TCP/IP service port used to query this
733 server.
734
735 If you click the `Probe for supported protocols'
736 button, fetchmail will try to find you the most
737 capable server on the selected host (this will
738 only work if you're conncted to the Internet).
739 The probe only checks for ordinary IMAP and POP
740 protocols; fortunately these are the most
741 frequently supported.
742
743 The `Protocol' button bar offers you a choice of
744 all the different protocols available.  The `auto'
745 protocol is the default mode; it probes the host
746 ports for POP3 and IMAP to see if either is
747 available.
748
749 Normally the TCP/IP service port to use is 
750 dictated by the protocol choice.  The `Port'
751 field (only present in expert mode) lets you
752 set a non-standard port.
753 """}
754
755 sechelp = {
756     'title' : 'Security option help',
757     'banner': 'Security',
758     'text' : """
759 The `interface' option, if given, specifies the only
760 device through which fetchmail is permitted to connect
761 to servers.  Specifying this may protect you from a 
762 spoofing attack if your client machine has more than
763 one IP gateway address and some of the gateways are
764 to insecure nets.
765
766 The `monitor' option allows you to specify a range
767 of IP addresses to monitor for activity.  If these
768 addresses are not active, fetchmail will not poll.
769 This option may be used to prevent fetchmail from
770 triggering an expensive dial-out if the interface
771 is not already active.
772
773 The `interface' and `monitor' options are available
774 only for Linux systems.  See the fetchmail manual page
775 for details on these.
776
777 The `netsec' option will be configurable only if fetchmail
778 was compiled with IPV6 support.  If you need to use it,
779 you probably know what to do.
780 """}
781
782 multihelp = {
783     'title' : 'Multidrop option help',
784     'banner': 'Multidrop',
785     'text' : """
786 These options are only useful with multidrop mode.
787 See the manual page for extended discussion.
788 """}
789
790 suserhelp = {
791     'title' : 'User list help',
792     'banner': 'User list',
793     'text' : """
794 When you add a user name to the list here, 
795 you initialize an entry telling fetchmail
796 to poll the site on behalf of the new user.
797
798 When you select a username (by double-
799 clicking it, or by single-clicking to
800 select and then clicking the Edit button),
801 you will open a window to configure the
802 user's options on that site.
803 """}
804
805 class ServerEdit(Frame, MyWidget):
806     def __init__(self, host, servers):
807         self.server = None
808         for site in servers:
809             if site.pollname == host:
810                 self.server = site
811         if (self.server == None):
812                 self.server = Server()
813                 self.server.pollname = host
814                 self.server.via = None
815                 servers.append(self.server)
816
817     def edit(self, mode, master=None):
818         Frame.__init__(self, master)
819         Pack.config(self)
820         self.master.title('Fetchmail host ' + self.server.pollname);
821         self.master.iconname('Fetchmail host ' + self.server.pollname);
822         self.post(Server, 'server')
823         self.makeWidgets(self.server.pollname, mode)
824         self.keepalive = []     # Use this to anchor the PhotoImage object
825         make_icon_window(self, fetchmail_gif)
826 #       self.grab_set()
827 #       self.focus_set()
828 #       self.wait_window()
829
830     def nosave(self):
831         if ConfirmQuit(self, 'server option editing'):
832             Widget.destroy(self.master)
833
834     def save(self):
835         self.fetch(Server, 'server')
836         Widget.destroy(self.master)
837
838     def refreshPort(self):
839         proto = self.protocol.get()
840         self.port.set(defaultports[proto])
841         if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED 
842
843     def makeWidgets(self, host, mode):
844         topwin = dispose_window(self, "Server options for querying " + host, serverhelp)
845
846         leftwin = Frame(self);
847         leftwidth = '25';
848
849         if mode != 'novice':
850             ctlwin = Frame(leftwin, relief=RAISED, bd=5)
851             Label(ctlwin, text="Run Controls").pack(side=TOP)
852             Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP)
853             LabeledEntry(ctlwin, 'True name of ' + host + ':',
854                       self.via, leftwidth).pack(side=TOP, fill=X)
855             LabeledEntry(ctlwin, 'Cycles to skip between polls:',
856                       self.interval, leftwidth).pack(side=TOP, fill=X)
857             LabeledEntry(ctlwin, 'Server timeout (seconds):',
858                       self.timeout, leftwidth).pack(side=TOP, fill=X)
859             Button(ctlwin, text='Help', fg='blue',
860                command=lambda: helpwin(controlhelp)).pack(side=RIGHT)
861             ctlwin.pack(fill=X)
862
863         protwin = Frame(leftwin, relief=RAISED, bd=5)
864         Label(protwin, text="Protocol").pack(side=TOP)
865         ButtonBar(protwin, '',
866                   self.protocol, protolist, 2,
867                   self.refreshPort) 
868         if mode != 'novice':
869             LabeledEntry(protwin, 'On server TCP/IP port:',
870                       self.port, leftwidth).pack(side=TOP, fill=X)
871             self.refreshPort()
872             Checkbutton(protwin,
873                 text="POP3: track `seen' with client-side UIDLs?",
874                 variable=self.uidl).pack(side=TOP)   
875         Button(protwin, text='Probe for supported protocols', fg='blue',
876                command=self.autoprobe).pack(side=LEFT)
877         Button(protwin, text='Help', fg='blue',
878                command=lambda: helpwin(protohelp)).pack(side=RIGHT)
879         protwin.pack(fill=X)
880
881         userwin = Frame(leftwin, relief=RAISED, bd=5)
882         Label(userwin, text="User entries for " + host).pack(side=TOP)
883         ListEdit("New user: ",
884                  map(lambda x: x.remote, self.server.users),
885                  lambda u, m=mode, s=self.server: UserEdit(u,s).edit(m, Toplevel()),
886                  userwin, suserhelp)
887         userwin.pack(fill=X)
888
889         leftwin.pack(side=LEFT, anchor=N, fill=X);
890
891         if mode != 'novice':
892             rightwin = Frame(self);
893
894             mdropwin = Frame(rightwin, relief=RAISED, bd=5)
895             Label(mdropwin, text="Multidrop options").pack(side=TOP)
896             LabeledEntry(mdropwin, 'Envelope address header:',
897                       self.envelope, '22').pack(side=TOP, fill=X)
898             LabeledEntry(mdropwin, 'Envelope headers to skip:',
899                       self.envskip, '22').pack(side=TOP, fill=X)
900             LabeledEntry(mdropwin, 'Name prefix to strip:',
901                       self.qvirtual, '22').pack(side=TOP, fill=X)
902             Checkbutton(mdropwin, text="Enable multidrop DNS lookup?",
903                     variable=self.dns).pack(side=TOP)
904             Label(mdropwin, text="DNS aliases").pack(side=TOP)
905             ListEdit("New alias: ", self.server.aka, None, mdropwin, None)
906             Label(mdropwin, text="Domains to be considered local").pack(side=TOP)
907             ListEdit("New domain: ",
908                  self.server.localdomains, None, mdropwin, multihelp)
909             mdropwin.pack(fill=X)
910
911             if 'interface' in dictmembers or 'monitor' in dictmembers or 'netsec' in dictmembers:
912                 secwin = Frame(rightwin, relief=RAISED, bd=5)
913                 Label(secwin, text="Security").pack(side=TOP)
914                 # Don't actually let users set this.  KPOP sets it implicitly
915                 #       ButtonBar(secwin, 'Authorization mode:',
916                 #                 self.auth, authlist, 1, None).pack(side=TOP)
917                 if 'interface' in dictmembers:
918                     LabeledEntry(secwin, 'Interface to check before poll:',
919                          self.interface, leftwidth).pack(side=TOP, fill=X)
920                 if 'monitor' in dictmembers:
921                     LabeledEntry(secwin, 'IP range to monitor:',
922                          self.monitor, leftwidth).pack(side=TOP, fill=X)
923                 if 'netsec' in dictmembers:
924                     LabeledEntry(secwin, 'IPV6 security options:',
925                          self.netsec, leftwidth).pack(side=TOP, fill=X)
926                     Button(secwin, text='Help', fg='blue',
927                            command=lambda: helpwin(sechelp)).pack(side=RIGHT)
928                 secwin.pack(fill=X)
929
930             rightwin.pack(side=LEFT, anchor=N);
931
932     def autoprobe(self):
933         # Note: this only handles case (1) near fetchmail.c:892
934         # We're assuming people smart enough to set up ssh tunneling
935         # won't need autoprobing.
936         if self.server.via != None:
937             realhost = self.server.via
938         else:
939             realhost = self.server.pollname
940         greetline = None
941         for (protocol, port) in (("IMAP",143), ("POP3",110), ("POP2",109)):
942             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
943             try:
944                 sock.connect(realhost, port)
945                 greetline = sock.recv(1024)
946                 sock.close()
947             except:
948                 pass
949             else:
950                 break
951         confwin = Toplevel()
952         if greetline == None:
953             title = "Autoprobe of " + realhost + " failed"
954             confirm = """
955 Fetchmailconf didn't find any mailservers active.
956 This could mean the host doesn't support any,
957 or that your Internet connection is down, or
958 that the host is so slow that the probe timed
959 out before getting a response.
960 """
961         else:
962             warnings = ''
963             # OK, now try to recognize potential problems
964
965             if protocol == "POP2":
966                 warnings = warnings + """
967 It appears you have somehow found a mailserver running only POP2.
968 Congratulations.  Have you considered a career in archaeology?
969
970 Unfortunately, stock fetchmail binaries don't include POP2 support anymore.
971 Unless the first line of your fetchmail -V output includes the string "POP2",
972 you'll have to build it from sources yourself with the configure
973 switch --enable-POP2.
974
975 """
976             if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.003") > 0:
977                 warnings = warnings + """
978 This appears to be an old version of the UC Davis POP server.  These are
979 dangerously unreliable (among other problems, they may drop your mailbox
980 on the floor if your connection is interrupted during the session).
981
982 It is strongly recommended that you find a better POP3 server.  The fetchmail
983 FAQ includes pointers to good ones.
984
985 """
986             if string.find(greetline, "usa.net") > 0:
987                 warnings = warnings + """
988 You appear to be using USA.NET's free mail service.  Their POP3 servers
989 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
990 fetchmail can compensate.  They seem to require that fetchall be switched on
991 (otherwise you won't necessarily see all your mail, not even new mail).
992 They also botch the TOP command the fetchmail normally uses for retrieval
993 (it only retrieves about 10 lines rather than the number specified).
994 Turning on fetchall will disable the use of TOP.
995
996 Therefore, it is strongly recommended that you turn on `fetchall' on all
997 user entries associated with this server.  
998
999 """
1000             if string.find(greetline, "QPOP") > 0:
1001                 warnings = warnings + """
1002 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1003 knows all about qpopper.
1004
1005 """
1006             closebrak = string.find(greetline, ">")
1007             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1008                 warnings = warnings + """
1009 It looks like you could use APOP on this server and avoid sending it your
1010 password in clear.  You should talk to the mailserver administrator about
1011 this.
1012
1013 """
1014             if string.find(greetline, "csi.com") > 0:
1015                 warnings = warnings + """
1016 It appears you're talking to CompuServe.  You can use their special RPA
1017 service for authentication, but only if your fetchmail -V output's first
1018 line contains the string "RPA".  This is not included in stock fetchmail
1019 binaries; to compile it in, rebuild from sources with the configure
1020 option --enable-RPA.
1021 """
1022             if string.find(greetline, "IMAP2bis") > 0:
1023                 warnings = warnings + """
1024 IMAP2bis servers have a minor problem; they can't peek at messages without
1025 marking them seen.  If you take a line hit during the retrieval, the 
1026 interrupted message may get left on the server, marked seen.
1027
1028 To work around this, it is recommended that you set the `fetchall'
1029 option on all user entries associated with this server, so any stuck
1030 mail will be retrieved next time around.
1031
1032 """
1033             if warnings == '':
1034                 warnings = warnings + """
1035 Fetchmail doesn't know anything special about this server type.
1036 """
1037
1038             # Display success window with warnings
1039             title = "Autoprobe of " + realhost + " succeeded"
1040             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1041             self.protocol.set(protocol)
1042         confwin.title(title) 
1043         confwin.iconname(title)
1044         Label(confwin, text=title).pack()
1045         Message(confwin, text=confirm, width=600).pack()
1046         Button(confwin, text='Done', 
1047                    command=lambda x=confwin: Widget.destroy(x),
1048                    relief=SUNKEN, bd=2).pack()
1049         
1050 #
1051 # User editing stuff
1052 #
1053
1054 userhelp = {
1055     'title' : 'User option help',
1056     'banner': 'User options',
1057     'text' : """
1058 You may use this panel to set options
1059 that may differ between individual
1060 users on your site.
1061
1062 Once you have a user configuration set
1063 up as you like it, you can select `Save' to
1064 store it in the server list maintained in
1065 the main configuration window.
1066
1067 If you wish to discard the changes you have
1068 made to user options, select `Quit'.
1069 """}
1070
1071 localhelp = {
1072     'title' : 'Local name help',
1073     'banner': 'Local names',
1074     'text' : """
1075 The local name(s) in a user entry are the
1076 people on the client machine who should
1077 receive mail from the poll described.
1078
1079 Note: if a user entry has more than one
1080 local name, messages will be retrieved
1081 in multidrop mode.  This complicates
1082 the configuration issues; see the manual
1083 page section on multidrop mode.
1084 """}
1085
1086 class UserEdit(Frame, MyWidget):
1087     def __init__(self, username, server):
1088         self.server = server
1089         self.user = None
1090         for user in server.users:
1091             if user.remote == username:
1092                 self.user = user
1093         if self.user == None:
1094             self.user = User()
1095             self.user.remote = username
1096             self.user.localnames = [username]
1097             server.users.append(self.user)
1098
1099     def edit(self, mode, master=None):
1100         Frame.__init__(self, master)
1101         Pack.config(self)
1102         self.master.title('Fetchmail user ' + self.user.remote
1103                           + ' querying ' + self.server.pollname);
1104         self.master.iconname('Fetchmail user ' + self.user.remote);
1105         self.post(User, 'user')
1106         self.makeWidgets(mode, self.server.pollname)
1107         self.keepalive = []     # Use this to anchor the PhotoImage object
1108         make_icon_window(self, fetchmail_gif)
1109 #       self.grab_set()
1110 #       self.focus_set()
1111 #       self.wait_window()
1112
1113     def nosave(self):
1114         if ConfirmQuit(self, 'user option editing'):
1115             Widget.destroy(self.master)
1116
1117     def save(self):
1118         self.fetch(User, 'user')
1119         Widget.destroy(self.master)
1120
1121     def makeWidgets(self, mode, servername):
1122         dispose_window(self,
1123                         "User options for " + self.user.remote + " querying " + servername,
1124                         userhelp)
1125
1126         if mode != 'novice':
1127             leftwin = Frame(self);
1128         else:
1129             leftwin = self
1130
1131         secwin = Frame(leftwin, relief=RAISED, bd=5)
1132         Label(secwin, text="Authentication").pack(side=TOP)
1133         LabeledEntry(secwin, 'Password:',
1134                       self.password, '12').pack(side=TOP, fill=X)
1135         secwin.pack(fill=X, anchor=N)
1136
1137         names = Frame(leftwin, relief=RAISED, bd=5)
1138         Label(names, text="Local names").pack(side=TOP)
1139         ListEdit("New name: ",
1140                      self.user.localnames, None, names, localhelp)
1141         names.pack(fill=X, anchor=N)
1142
1143         if mode != 'novice':
1144             targwin = Frame(leftwin, relief=RAISED, bd=5)
1145             Label(targwin, text="Forwarding Options").pack(side=TOP)
1146             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1147             ListEdit("New listener:", self.user.smtphunt, None, targwin, None)
1148             LabeledEntry(targwin, 'Append to MAIL FROM line:',
1149                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1150             LabeledEntry(targwin, 'Connection setup command:',
1151                      self.preconnect, '26').pack(side=TOP, fill=X)
1152             LabeledEntry(targwin, 'Connection wrapup command:',
1153                      self.postconnect, '26').pack(side=TOP, fill=X)
1154             LabeledEntry(targwin, 'Local delivery agent:',
1155                      self.mda, '26').pack(side=TOP, fill=X)
1156             LabeledEntry(targwin, 'Listener spam-block codes:',
1157                      self.antispam, '26').pack(side=TOP, fill=X)
1158             targwin.pack(fill=X, anchor=N)
1159
1160         if mode != 'novice':
1161             leftwin.pack(side=LEFT, fill=X, anchor=N)
1162             rightwin = Frame(self)
1163         else:
1164             rightwin = self
1165
1166         optwin = Frame(rightwin, relief=RAISED, bd=5)
1167         Label(optwin, text="Processing Options").pack(side=TOP)
1168         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1169                     variable=self.keep).pack(side=TOP, anchor=W)
1170         Checkbutton(optwin, text="Fetch old messages as well as new",
1171                     variable=self.fetchall).pack(side=TOP, anchor=W)
1172         if mode != 'novice':
1173             Checkbutton(optwin, text="Flush seen messages before retrieval", 
1174                     variable=self.flush).pack(side=TOP, anchor=W)
1175             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply", 
1176                     variable=self.rewrite).pack(side=TOP, anchor=W)
1177             Checkbutton(optwin, text="Force CR/LF at end of each line",
1178                     variable=self.forcecr).pack(side=TOP, anchor=W)
1179             Checkbutton(optwin, text="Strip CR from end of eacgh line",
1180                     variable=self.stripcr).pack(side=TOP, anchor=W)
1181             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1182                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1183             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1184                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1185             Checkbutton(optwin, text="Drop Status lines from forwarded messages", 
1186                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1187         optwin.pack(fill=X)
1188
1189         if mode != 'novice':
1190             limwin = Frame(rightwin, relief=RAISED, bd=5)
1191             Label(limwin, text="Resource Limits").pack(side=TOP)
1192             LabeledEntry(limwin, 'Message size limit:',
1193                       self.limit, '30').pack(side=TOP, fill=X)
1194             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1195                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1196             LabeledEntry(limwin, 'Max messages to forward per poll:',
1197                       self.batchlimit, '30').pack(side=TOP, fill=X)
1198             LabeledEntry(limwin, 'Interval between expunges (IMAP):',
1199                       self.expunge, '30').pack(side=TOP, fill=X)
1200             limwin.pack(fill=X)
1201
1202             foldwin = Frame(rightwin, relief=RAISED, bd=5)
1203             Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1204             ListEdit("New folder:", self.user.mailboxes, None, foldwin, None)
1205             foldwin.pack(fill=X, anchor=N)
1206
1207         if mode != 'novice':
1208             rightwin.pack(side=LEFT)
1209         else:
1210             self.pack()
1211
1212
1213 #
1214 # Top-level window that offers either novice or expert mode
1215 # (but not both at once; it disappears when one is selected).
1216 #
1217
1218 class MainWindow(Frame):
1219     def __init__(self, outfile, master=None):
1220         Frame.__init__(self, master)
1221         self.outfile = outfile
1222         self.master.title('fetchmail configurator main');
1223         self.master.iconname('fetchmail configurator main');
1224         Pack.config(self)
1225         Label(self,
1226                 text='Fetchmailconf ' + version, 
1227                 bd=2).pack(side=TOP, pady=10)
1228         self.keepalive = []     # Use this to anchor the PhotoImage object
1229         make_icon_window(self, fetchmail_gif)
1230
1231         Message(self, text="""
1232 Use `Novice Configuration' for basic fetchmail setup;
1233 with this, you can easily set up a single-drop connection
1234 to one remote mail server.
1235 """, width=600).pack(side=TOP)
1236         Button(self, text='Novice Configuration',
1237                                 fg='blue', command=self.novice).pack()
1238
1239         Message(self, text="""
1240 Use `Expert Configuration' for advanced fetchmail setup,
1241 including multiple-site or multidrop connections.
1242 """, width=600).pack(side=TOP)
1243         Button(self, text='Expert Configuration',
1244                                 fg='blue', command=self.expert).pack()
1245
1246         Message(self, text="""
1247 Or you can just select `Quit' to leave the configurator now.
1248 """, width=600).pack(side=TOP)
1249         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1250
1251     def novice(self):
1252         self.destroy()
1253         ConfigurationEdit(Fetchmailrc, self.outfile).edit('novice')
1254
1255     def expert(self):
1256         self.destroy()
1257         ConfigurationEdit(Fetchmailrc, self.outfile).edit('expert')
1258
1259     def leave(self):
1260         self.quit()
1261
1262 # Functions for turning a dictionary into an instantiated object tree.
1263
1264 def intersect(list1, list2):
1265 # Compute set intersection of lists
1266     res = []
1267     for x in list1:
1268         if x in list2:
1269             res.append(x)
1270     return res
1271
1272 def setdiff(list1, list2):
1273 # Compute set difference of lists
1274     res = []
1275     for x in list1:
1276         if not x in list2:
1277             res.append(x)
1278     return res
1279
1280 def copy_instance(toclass, fromdict):
1281 # Initialize a class object of given type from a conformant dictionary.
1282     for fld in fromdict.keys():
1283         if not fld in dictmembers:
1284             dictmembers.append(fld)
1285 # The `optional' fields are the ones we can ignore for purposes of
1286 # conformability checking; they'll still get copied if they are
1287 # present in the dictionary.
1288     optional = ('interface', 'monitor', 'netsec');
1289     class_sig = setdiff(toclass.__dict__.keys(), optional)
1290     class_sig.sort()
1291     dict_keys = setdiff(fromdict.keys(), optional)
1292     dict_keys.sort()
1293     common = intersect(class_sig, dict_keys)
1294     if 'typemap' in class_sig: 
1295         class_sig.remove('typemap')
1296     if tuple(class_sig) != tuple(dict_keys):
1297         print "Fields don't match what fetchmailconf expected:"
1298 #       print "Class signature: " + `class_sig`
1299 #       print "Dictionary keys: " + `dict_keys`
1300         diff = setdiff(class_sig, common)
1301         if diff:
1302             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
1303         diff = setdiff(dict_keys, common)
1304         if diff:
1305             print "Not matched in dictionary keys: " + `diff`
1306         sys.exit(1)
1307     else:
1308         for x in dict_keys:
1309             setattr(toclass, x, fromdict[x])
1310
1311 #
1312 # And this is the main sequence.  How it works:  
1313 #
1314 # First, call `fetchmail --configdump' and trap the output in a tempfile.
1315 # This should fill it with a Python initializer for a variable `fetchmailrc'.
1316 # Run execfile on the file to pull fetchmailrc into Python global space.
1317 # You don't want static data, though; you want, instead, a tree of objects
1318 # with the same data members and added appropriate methods.
1319 #
1320 # This is what the copy_instance function() is for.  It tries to copy a
1321 # dictionary field by field into a class, aborting if the class and dictionary
1322 # have different data members (except for any typemap member in the class;
1323 # that one is strictly for use by the MyWidget supperclass).
1324 #
1325 # Once the object tree is set up, require user to choose novice or expert
1326 # mode and instantiate an edit object for the configuration.  Class methods
1327 # will take it all from there.
1328 #
1329 # Options (not documented because they're for fetchmailconf debuggers only):
1330 # -d: Read the configuration and dump it to stdout before editing.  Dump
1331 #     the edited result to stdout as well.
1332 # -f: specify the run control file to read.
1333
1334 if __name__ == '__main__': 
1335
1336     fetchmail_gif = """
1337 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
1338 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
1339 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
1340 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
1341 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
1342 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
1343 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
1344 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
1345 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
1346 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
1347 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
1348 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
1349 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
1350 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
1351 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
1352 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
1353 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
1354 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
1355 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
1356 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
1357 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
1358 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
1359 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
1360 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
1361 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
1362 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
1363 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
1364 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
1365 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
1366 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
1367 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
1368 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
1369 """
1370
1371     # Process options
1372     (options, arguments) = getopt.getopt(sys.argv[1:], "df:")
1373     dump = rcfile = None;
1374     for (switch, val) in options:
1375         if (switch == '-d'):
1376             dump = TRUE
1377         elif (switch == '-f'):
1378             rcfile = val
1379
1380     # Get client host's FQDN
1381     hostname = socket.gethostbyaddr(socket.gethostname())[0]
1382
1383     # Compute defaults
1384     ConfigurationDefaults = Configuration()
1385     ServerDefaults = Server()
1386     UserDefaults = User()
1387
1388     # Read the existing configuration
1389     tmpfile = "/tmp/fetchmailconf." + `os.getpid()`
1390     if rcfile:
1391         cmd = "fetchmail -f " + rcfile + " --configdump >" + tmpfile
1392     else:
1393         cmd = "fetchmail --configdump >" + tmpfile
1394         
1395     try:
1396         s = os.system(cmd)
1397         if s != 0:
1398             print "`" + cmd + "' run failure, status " + `s`
1399             raise SystemExit
1400     except:
1401         print "Unknown error while running fetchmail --configdump"
1402         os.remove(tmpfile)
1403         sys.exit(1)
1404
1405     try:
1406         execfile(tmpfile)
1407     except:
1408         print "Can't read configuration output of fetchmail --configdump."
1409         os.remove(tmpfile)
1410         sys.exit(1)
1411         
1412     os.remove(tmpfile)
1413
1414     # The tricky part -- initializing objects from the configuration global
1415     # `Configuration' is the top level of the object tree we're going to mung.
1416     # The dictmembers list is used to track the set of fields the dictionary
1417     # contains; in particular, we can use it to tell whether things like the
1418     # monitor, interface, and netsec fields are present.
1419     dictmembers = []
1420     Fetchmailrc = Configuration()
1421     copy_instance(Fetchmailrc, fetchmailrc)
1422     Fetchmailrc.servers = [];
1423     for server in fetchmailrc['servers']:
1424         Newsite = Server()
1425         copy_instance(Newsite, server)
1426         Fetchmailrc.servers.append(Newsite)
1427         Newsite.users = [];
1428         for user in server['users']:
1429             Newuser = User()
1430             copy_instance(Newuser, user)
1431             Newsite.users.append(Newuser)
1432
1433     # We may want to display the configuration and quit
1434     if dump:
1435         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
1436
1437     # The theory here is that -f alone sets the rcfile location,
1438     # but -d and -f together mean the new configuration should go to stdout.
1439     if not rcfile and not dump:
1440         rcfile = os.environ["HOME"] + "/.fetchmailrc"
1441
1442     # OK, now run the configuration edit
1443     root = MainWindow(rcfile)
1444     root.mainloop()
1445
1446 # The following sets edit modes for GNU EMACS
1447 # Local Variables:
1448 # mode:python
1449 # End: