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