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