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