]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf
c5b6113112725ddeff53da52a81a7159d8de1431
[~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.15"
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.auth = '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             ('auth',      '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.auth != ServerDefaults.auth:
136             str = str + " auth " + self.auth
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 authlist = ("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.keepalive = []     # Use this to anchor the PhotoImage object
651         make_icon_window(self, fetchmail_gif)
652         Pack.config(self)
653         self.post(Configuration, 'configuration')
654
655         dispose_window(self,
656                        'Configurator ' + self.mode + ' Controls',
657                        ConfigurationEdit.mode_to_help[self.mode],
658                        'Save')
659
660         gf = Frame(self, relief=RAISED, bd = 5)
661         Label(gf,
662                 text='Fetchmail Run Controls', 
663                 bd=2).pack(side=TOP, pady=10)
664
665         df = Frame(gf)
666
667         ff = Frame(df)
668         if self.mode != 'novice':
669             # Set the postmaster
670             log = LabeledEntry(ff, '     Postmaster:', self.postmaster, '14')
671             log.pack(side=RIGHT, anchor=E)
672
673         # Set the poll interval
674         de = LabeledEntry(ff, '     Poll interval:', self.poll_interval, '14')
675         de.pack(side=RIGHT, anchor=E)
676         ff.pack()
677
678         df.pack()
679
680         if self.mode != 'novice':
681             pf = Frame(gf)
682             Checkbutton(pf,
683                 {'text':'Bounces to sender?',
684                 'variable':self.bouncemail,
685                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
686             pf.pack(fill=X)
687
688             sf = Frame(gf)
689             Checkbutton(sf,
690                 {'text':'Log to syslog?',
691                 'variable':self.syslog,
692                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
693             log = LabeledEntry(sf, '     Logfile:', self.logfile, '14')
694             log.pack(side=RIGHT, anchor=E)
695             sf.pack(fill=X)
696
697             Checkbutton(gf,
698                 {'text':'Invisible mode?',
699                 'variable':self.invisible,
700                  'relief':GROOVE}).pack(side=LEFT, anchor=W)
701             # Set the idfile
702             log = LabeledEntry(gf, '     Idfile:', self.idfile, '14')
703             log.pack(side=RIGHT, anchor=E)
704
705         gf.pack(fill=X)
706
707         # Expert mode allows us to edit multiple sites
708         lf = Frame(self, relief=RAISED, bd=5)
709         Label(lf,
710               text='Remote Mail Server Configurations', 
711               bd=2).pack(side=TOP, pady=10)
712         ListEdit('New Server:', 
713                 map(lambda x: x.pollname, self.configuration.servers),
714                 lambda site, self=self: self.server_edit(site),
715                 lambda site, self=self: self.server_delete(site),
716                 lf, remotehelp)
717         lf.pack(fill=X)
718
719     def destruct(self):
720         for sitename in self.subwidgets.keys():
721             self.subwidgets[sitename].destruct()        
722         self.master.destroy()
723         self.onexit()
724
725     def nosave(self):
726         if ConfirmQuit(self, self.mode + " configuration editor"):
727             self.destruct()
728
729     def save(self):
730         for sitename in self.subwidgets.keys():
731             self.subwidgets[sitename].save()
732         self.fetch(Configuration, 'configuration')
733         fm = None
734         if not self.outfile:
735             fm = sys.stdout
736         elif not os.path.isfile(self.outfile) or Dialog(self, 
737                  title = 'Overwrite existing run control file?',
738                  text = 'Really overwrite existing run control file?',
739                  bitmap = 'question',
740                  strings = ('Yes', 'No'),
741                  default = 1).num == 0:
742             fm = open(self.outfile, 'w')
743         if fm:
744             fm.write("# Configuration created %s by fetchmailconf\n" % time.ctime(time.time()))
745             fm.write(`self.configuration`)
746             if self.outfile:
747                 fm.close()
748             if fm != sys.stdout:
749                 os.chmod(self.outfile, 0600)
750             self.destruct()
751
752 #
753 # Server editing stuff.
754 #
755 remotehelp = {
756     'title' : 'Remote site help',
757     'banner': 'Remote sites',
758     'text' : """
759 When you add a site name to the list here, 
760 you initialize an entry telling fetchmail
761 how to poll a new site.
762
763 When you select a sitename (by double-
764 clicking it, or by single-clicking to
765 select and then clicking the Edit button),
766 you will open a window to configure that
767 site.
768 """}
769
770 serverhelp = {
771     'title' : 'Server options help',
772     'banner': 'Server Options',
773     'text' : """
774 The server options screen controls fetchmail 
775 options that apply to one of your mailservers.
776
777 Once you have a mailserver configuration set
778 up as you like it, you can select `OK' to
779 store it in the server list maintained in
780 the main configuration window.
781
782 If you wish to discard changes to a server 
783 configuration, select `Quit'.
784 """}
785
786 controlhelp = {
787     'title' : 'Run Control help',
788     'banner': 'Run Controls',
789     'text' : """
790 If the `Poll normally' checkbox is on, the host is polled as part of
791 the normal operation of fetchmail when it is run with no arguments.
792 If it is off, fetchmail will only query this host when it is given as
793 a command-line argument.
794
795 The `True name of server' box should specify the actual DNS name
796 to query. By default this is the same as the poll name.
797
798 Normally each host described in the file is queried once each 
799 poll cycle. If `Cycles to skip between polls' is greater than 0,
800 that's the number of poll cycles that are skipped between the
801 times this post is actually polled.
802
803 The `Server timeout' is the number of seconds fetchmail will wait
804 for a reply from the mailserver before concluding it is hung and
805 giving up.
806 """}
807
808 protohelp = {
809     'title' : 'Protocol and Port help',
810     'banner': 'Protocol and Port',
811     'text' : """
812 These options control the remote-mail protocol
813 and TCP/IP service port used to query this
814 server.
815
816 If you click the `Probe for supported protocols'
817 button, fetchmail will try to find you the most
818 capable server on the selected host (this will
819 only work if you're conncted to the Internet).
820 The probe only checks for ordinary IMAP and POP
821 protocols; fortunately these are the most
822 frequently supported.
823
824 The `Protocol' button bar offers you a choice of
825 all the different protocols available.  The `auto'
826 protocol is the default mode; it probes the host
827 ports for POP3 and IMAP to see if either is
828 available.
829
830 Normally the TCP/IP service port to use is 
831 dictated by the protocol choice.  The `Port'
832 field (only present in expert mode) lets you
833 set a non-standard port.
834 """}
835
836 sechelp = {
837     'title' : 'Security option help',
838     'banner': 'Security',
839     'text' : """
840 The `interface' option allows you to specify a range
841 of IP addresses to monitor for activity.  If these
842 addresses are not active, fetchmail will not poll.
843 Specifying this may protect you from a spoofing attack
844 if your client machine has more than one IP gateway
845 address and some of the gateways are to insecure nets.
846
847 The `monitor' option, if given, specifies the only
848 device through which fetchmail is permitted to connect
849 to servers.  This option may be used to prevent
850 fetchmail from triggering an expensive dial-out if the
851 interface is not already active.
852
853 The `interface' and `monitor' options are available
854 only for Linux and freeBSD systems.  See the fetchmail
855 manual page for details on these.
856
857 The `netsec' option will be configurable only if fetchmail
858 was compiled with IPV6 support.  If you need to use it,
859 you probably know what to do.
860 """}
861
862 multihelp = {
863     'title' : 'Multidrop option help',
864     'banner': 'Multidrop',
865     'text' : """
866 These options are only useful with multidrop mode.
867 See the manual page for extended discussion.
868 """}
869
870 suserhelp = {
871     'title' : 'User list help',
872     'banner': 'User list',
873     'text' : """
874 When you add a user name to the list here, 
875 you initialize an entry telling fetchmail
876 to poll the site on behalf of the new user.
877
878 When you select a username (by double-
879 clicking it, or by single-clicking to
880 select and then clicking the Edit button),
881 you will open a window to configure the
882 user's options on that site.
883 """}
884
885 class ServerEdit(Frame, MyWidget):
886     def __init__(self, host, parent):
887         self.parent = parent
888         self.server = None
889         self.subwidgets = {}
890         for site in parent.configuration.servers:
891             if site.pollname == host:
892                 self.server = site
893         if (self.server == None):
894                 self.server = Server()
895                 self.server.pollname = host
896                 self.server.via = None
897                 parent.configuration.servers.append(self.server)
898
899     def edit(self, mode, master=None):
900         Frame.__init__(self, master)
901         Pack.config(self)
902         self.master.title('Fetchmail host ' + self.server.pollname);
903         self.master.iconname('Fetchmail host ' + self.server.pollname);
904         self.post(Server, 'server')
905         self.makeWidgets(self.server.pollname, mode)
906         self.keepalive = []     # Use this to anchor the PhotoImage object
907         make_icon_window(self, fetchmail_gif)
908 #       self.grab_set()
909 #       self.focus_set()
910 #       self.wait_window()
911         return self
912
913     def destruct(self):
914         for username in self.subwidgets.keys():
915             self.subwidgets[username].destruct()        
916         del self.parent.subwidgets[self.server.pollname]
917         Widget.destroy(self.master)
918
919     def nosave(self):
920         if ConfirmQuit(self, 'server option editing'):
921             self.destruct()
922
923     def save(self):
924         self.fetch(Server, 'server')
925         for username in self.subwidgets.keys():
926             self.subwidgets[username].save()        
927         self.destruct()
928
929     def refreshPort(self):
930         proto = self.protocol.get()
931         if self.port.get() == 0:
932             self.port.set(defaultports[proto])
933         if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED 
934
935     def user_edit(self, username, mode):
936         self.subwidgets[username] = UserEdit(username, self).edit(mode, Toplevel())
937
938     def user_delete(self, username):
939         if self.subwidgets.has_key(username):
940             del self.subwidgets[username]
941         del self.server[username]
942
943     def makeWidgets(self, host, mode):
944         topwin = dispose_window(self, "Server options for querying " + host, serverhelp)
945
946         leftwin = Frame(self);
947         leftwidth = '25';
948
949         if mode != 'novice':
950             ctlwin = Frame(leftwin, relief=RAISED, bd=5)
951             Label(ctlwin, text="Run Controls").pack(side=TOP)
952             Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP)
953             LabeledEntry(ctlwin, 'True name of ' + host + ':',
954                       self.via, leftwidth).pack(side=TOP, fill=X)
955             LabeledEntry(ctlwin, 'Cycles to skip between polls:',
956                       self.interval, leftwidth).pack(side=TOP, fill=X)
957             LabeledEntry(ctlwin, 'Server timeout (seconds):',
958                       self.timeout, leftwidth).pack(side=TOP, fill=X)
959             Button(ctlwin, text='Help', fg='blue',
960                command=lambda: helpwin(controlhelp)).pack(side=RIGHT)
961             ctlwin.pack(fill=X)
962
963         # Compute the available protocols from the compile-time options
964         protolist = ['auto']
965         if 'pop2' in feature_options:
966             protolist.append("POP2")
967         if 'pop3' in feature_options:
968             protolist = protolist + ["POP3", "APOP", "KPOP"]
969         if 'sdps' in feature_options:
970             protolist.append("SDPS")
971         if 'imap' in feature_options:
972             protolist.append("IMAP")
973         if 'imap-gss' in feature_options:
974             protolist.append("IMAP-GSS")
975         if 'imap-k4' in feature_options:
976             protolist.append("IMAP-K4")
977         if 'etrn' in feature_options:
978             protolist.append("ETRN")
979
980         protwin = Frame(leftwin, relief=RAISED, bd=5)
981         Label(protwin, text="Protocol").pack(side=TOP)
982         ButtonBar(protwin, '',
983                   self.protocol, protolist, 2,
984                   self.refreshPort) 
985         if mode != 'novice':
986             LabeledEntry(protwin, 'On server TCP/IP port:',
987                       self.port, leftwidth).pack(side=TOP, fill=X)
988             self.refreshPort()
989             Checkbutton(protwin,
990                 text="POP3: track `seen' with client-side UIDLs?",
991                 variable=self.uidl).pack(side=TOP)   
992         Button(protwin, text='Probe for supported protocols', fg='blue',
993                command=self.autoprobe).pack(side=LEFT)
994         Button(protwin, text='Help', fg='blue',
995                command=lambda: helpwin(protohelp)).pack(side=RIGHT)
996         protwin.pack(fill=X)
997
998         userwin = Frame(leftwin, relief=RAISED, bd=5)
999         Label(userwin, text="User entries for " + host).pack(side=TOP)
1000         ListEdit("New user: ",
1001                  map(lambda x: x.remote, self.server.users),
1002                  lambda u, m=mode, s=self: s.user_edit(u, m),
1003                  lambda u, s=self: s.user_delete(u),
1004                  userwin, suserhelp)
1005         userwin.pack(fill=X)
1006
1007         leftwin.pack(side=LEFT, anchor=N, fill=X);
1008
1009         if mode != 'novice':
1010             rightwin = Frame(self);
1011
1012             mdropwin = Frame(rightwin, relief=RAISED, bd=5)
1013             Label(mdropwin, text="Multidrop options").pack(side=TOP)
1014             LabeledEntry(mdropwin, 'Envelope address header:',
1015                       self.envelope, '22').pack(side=TOP, fill=X)
1016             LabeledEntry(mdropwin, 'Envelope headers to skip:',
1017                       self.envskip, '22').pack(side=TOP, fill=X)
1018             LabeledEntry(mdropwin, 'Name prefix to strip:',
1019                       self.qvirtual, '22').pack(side=TOP, fill=X)
1020             Checkbutton(mdropwin, text="Enable multidrop DNS lookup?",
1021                     variable=self.dns).pack(side=TOP)
1022             Label(mdropwin, text="DNS aliases").pack(side=TOP)
1023             ListEdit("New alias: ", self.server.aka, None, None, mdropwin, None)
1024             Label(mdropwin, text="Domains to be considered local").pack(side=TOP)
1025             ListEdit("New domain: ",
1026                  self.server.localdomains, None, None, mdropwin, multihelp)
1027             mdropwin.pack(fill=X)
1028
1029             if os_type == 'linux' or os_type == 'freebsd' or 'netsec' in feature_options:
1030                 secwin = Frame(rightwin, relief=RAISED, bd=5)
1031                 Label(secwin, text="Security").pack(side=TOP)
1032                 # Don't actually let users set this.  KPOP sets it implicitly
1033                 #       ButtonBar(secwin, 'Authorization mode:',
1034                 #                 self.auth, authlist, 1, None).pack(side=TOP)
1035                 if os_type == 'linux' or os_type == 'freebsd'  or 'interface' in dictmembers:
1036                     LabeledEntry(secwin, 'IP range to check before poll:',
1037                          self.interface, leftwidth).pack(side=TOP, fill=X)
1038                 if os_type == 'linux' or os_type == 'freebsd' or 'monitor' in dictmembers:
1039                     LabeledEntry(secwin, 'Interface to monitor:',
1040                          self.monitor, leftwidth).pack(side=TOP, fill=X)
1041                 if 'netsec' in feature_options or 'netsec' in dictmembers:
1042                     LabeledEntry(secwin, 'IPV6 security options:',
1043                          self.netsec, leftwidth).pack(side=TOP, fill=X)
1044                 Button(secwin, text='Help', fg='blue',
1045                        command=lambda: helpwin(sechelp)).pack(side=RIGHT)
1046                 secwin.pack(fill=X)
1047
1048             rightwin.pack(side=LEFT, anchor=N);
1049
1050     def autoprobe(self):
1051         # Note: this only handles case (1) near fetchmail.c:1032
1052         # We're assuming people smart enough to set up ssh tunneling
1053         # won't need autoprobing.
1054         if self.server.via:
1055             realhost = self.server.via
1056         else:
1057             realhost = self.server.pollname
1058         greetline = None
1059         for (protocol, port) in (("IMAP",143), ("POP3",110), ("POP2",109)):
1060             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1061             try:
1062                 sock.connect(realhost, port)
1063                 greetline = sock.recv(1024)
1064                 sock.close()
1065             except:
1066                 pass
1067             else:
1068                 break
1069         confwin = Toplevel()
1070         if greetline == None:
1071             title = "Autoprobe of " + realhost + " failed"
1072             confirm = """
1073 Fetchmailconf didn't find any mailservers active.
1074 This could mean the host doesn't support any,
1075 or that your Internet connection is down, or
1076 that the host is so slow that the probe timed
1077 out before getting a response.
1078 """
1079         else:
1080             warnings = ''
1081             # OK, now try to recognize potential problems
1082
1083             if protocol == "POP2":
1084                 warnings = warnings + """
1085 It appears you have somehow found a mailserver running only POP2.
1086 Congratulations.  Have you considered a career in archaeology?
1087
1088 Unfortunately, stock fetchmail binaries don't include POP2 support anymore.
1089 Unless the first line of your fetchmail -V output includes the string "POP2",
1090 you'll have to build it from sources yourself with the configure
1091 switch --enable-POP2.
1092
1093 """
1094             if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.004") > 0:
1095                 warnings = warnings + """
1096 This appears to be an old version of the UC Davis POP server.  These are
1097 dangerously unreliable (among other problems, they may drop your mailbox
1098 on the floor if your connection is interrupted during the session).
1099
1100 It is strongly recommended that you find a better POP3 server.  The fetchmail
1101 FAQ includes pointers to good ones.
1102
1103 """
1104             if string.find(greetline, "usa.net") > 0:
1105                 warnings = warnings + """
1106 You appear to be using USA.NET's free mail service.  Their POP3 servers
1107 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
1108 fetchmail can compensate.  They seem to require that fetchall be switched on
1109 (otherwise you won't necessarily see all your mail, not even new mail).
1110 They also botch the TOP command the fetchmail normally uses for retrieval
1111 (it only retrieves about 10 lines rather than the number specified).
1112 Turning on fetchall will disable the use of TOP.
1113
1114 Therefore, it is strongly recommended that you turn on `fetchall' on all
1115 user entries associated with this server.  
1116
1117 """
1118             if string.find(greetline, "sprynet.com") > 0:
1119                 warnings = warnings + """
1120 You appear to be using a SpryNet server.  In mid-1999 it was reported that
1121 the SpryNet TOP command marks messages seen.  Therefore, for proper error
1122 recovery in the event of a line drop, it is strongly recommended that you
1123 turn on `fetchall' on all user entries associated with this server.  
1124
1125 """
1126
1127 # Steve VanDevender <stevev@efn.org> writes:
1128 # The only system I have seen this happen with is cucipop-1.31
1129 # under SunOS 4.1.4.  cucipop-1.31 runs fine on at least Solaris
1130 # 2.x and probably quite a few other systems.  It appears to be a
1131 # bug or bad interaction with the SunOS realloc() -- it turns out
1132 # that internally cucipop does allocate a certain data structure in
1133 # multiples of 16, using realloc() to bump it up to the next
1134 # multiple if it needs more.
1135
1136 # The distinctive symptom is that when there are 16 messages in the
1137 # inbox, you can RETR and DELE all 16 messages successfully, but on
1138 # QUIT cucipop returns something like "-ERR Error locking your
1139 # mailbox" and aborts without updating it.
1140
1141 # The cucipop banner looks like:
1142
1143 # +OK Cubic Circle's v1.31 1998/05/13 POP3 ready <6229000062f95036@wakko>
1144 #
1145             if string.find(greetline, "Cubic Circle") > 0:
1146                 warnings = warnings + """
1147 I see your server is running cucipop.  Better make sure the server box
1148 isn't a SunOS 4.1.4 machine; cucipop tickles a bug in SunOS realloc()
1149 under that version, and doesn't cope with the result gracefully.  Newer
1150 SunOS and Solaris machines run cucipop OK.
1151 """
1152             if string.find(greetline, "QPOP") > 0:
1153                 warnings = warnings + """
1154 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1155 knows all about qpopper.
1156
1157 """
1158             closebrak = string.find(greetline, ">")
1159             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1160                 warnings = warnings + """
1161 It looks like you could use APOP on this server and avoid sending it your
1162 password in clear.  You should talk to the mailserver administrator about
1163 this.
1164
1165 """
1166             if string.find(greetline, "csi.com") > 0:
1167                 warnings = warnings + """
1168 It appears you're talking to CompuServe.  You can use their special RPA
1169 service for authentication, but only if your fetchmail -V output's first
1170 line contains the string "RPA".  This is not included in stock fetchmail
1171 binaries; to compile it in, rebuild from sources with the configure
1172 option --enable-RPA.
1173 """
1174             if string.find(greetline, "IMAP2bis") > 0:
1175                 warnings = warnings + """
1176 IMAP2bis servers have a minor problem; they can't peek at messages without
1177 marking them seen.  If you take a line hit during the retrieval, the 
1178 interrupted message may get left on the server, marked seen.
1179
1180 To work around this, it is recommended that you set the `fetchall'
1181 option on all user entries associated with this server, so any stuck
1182 mail will be retrieved next time around.
1183
1184 """
1185             if string.find(greetline, "POP3 Server Ready") > 0:
1186                 warnings = warnings + """
1187 Some server that uses this greeting line has been observed to choke on
1188 TOP %d 99999999.  Use the fetchall option. if necessary, to force RETR.
1189 """
1190             if string.find(greetline, "IMAP4rev1") > 0:
1191                 warnings = warnings + """
1192 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1193 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1194 has therefore been extremely well tested with this class of server.
1195 """
1196             if warnings == '':
1197                 warnings = warnings + """
1198 Fetchmail doesn't know anything special about this server type.
1199 """
1200
1201             # Display success window with warnings
1202             title = "Autoprobe of " + realhost + " succeeded"
1203             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1204             self.protocol.set(protocol)
1205         confwin.title(title) 
1206         confwin.iconname(title)
1207         Label(confwin, text=title).pack()
1208         Message(confwin, text=confirm, width=600).pack()
1209         Button(confwin, text='Done', 
1210                    command=lambda x=confwin: Widget.destroy(x), bd=2).pack()
1211         
1212 #
1213 # User editing stuff
1214 #
1215
1216 userhelp = {
1217     'title' : 'User option help',
1218     'banner': 'User options',
1219     'text' : """
1220 You may use this panel to set options
1221 that may differ between individual
1222 users on your site.
1223
1224 Once you have a user configuration set
1225 up as you like it, you can select `OK' to
1226 store it in the user list maintained in
1227 the site configuration window.
1228
1229 If you wish to discard the changes you have
1230 made to user options, select `Quit'.
1231 """}
1232
1233 localhelp = {
1234     'title' : 'Local name help',
1235     'banner': 'Local names',
1236     'text' : """
1237 The local name(s) in a user entry are the
1238 people on the client machine who should
1239 receive mail from the poll described.
1240
1241 Note: if a user entry has more than one
1242 local name, messages will be retrieved
1243 in multidrop mode.  This complicates
1244 the configuration issues; see the manual
1245 page section on multidrop mode.
1246 """}
1247
1248 class UserEdit(Frame, MyWidget):
1249     def __init__(self, username, parent):
1250         self.parent = parent
1251         self.user = None
1252         for user in parent.server.users:
1253             if user.remote == username:
1254                 self.user = user
1255         if self.user == None:
1256             self.user = User()
1257             self.user.remote = username
1258             self.user.localnames = [username]
1259             parent.server.users.append(self.user)
1260
1261     def edit(self, mode, master=None):
1262         Frame.__init__(self, master)
1263         Pack.config(self)
1264         self.master.title('Fetchmail user ' + self.user.remote
1265                           + ' querying ' + self.parent.server.pollname);
1266         self.master.iconname('Fetchmail user ' + self.user.remote);
1267         self.post(User, 'user')
1268         self.makeWidgets(mode, self.parent.server.pollname)
1269         self.keepalive = []     # Use this to anchor the PhotoImage object
1270         make_icon_window(self, fetchmail_gif)
1271 #       self.grab_set()
1272 #       self.focus_set()
1273 #       self.wait_window()
1274         return self
1275
1276     def destruct(self):
1277         del self.parent.subwidgets[self.user.remote]
1278         Widget.destroy(self.master)
1279
1280     def nosave(self):
1281         if ConfirmQuit(self, 'user option editing'):
1282             self.destruct()
1283
1284     def save(self):
1285         self.fetch(User, 'user')
1286         self.destruct()
1287
1288     def makeWidgets(self, mode, servername):
1289         dispose_window(self,
1290                         "User options for " + self.user.remote + " querying " + servername,
1291                         userhelp)
1292
1293         if mode != 'novice':
1294             leftwin = Frame(self);
1295         else:
1296             leftwin = self
1297
1298         secwin = Frame(leftwin, relief=RAISED, bd=5)
1299         Label(secwin, text="Authentication").pack(side=TOP)
1300         LabeledEntry(secwin, 'Password:',
1301                       self.password, '12').pack(side=TOP, fill=X)
1302         secwin.pack(fill=X, anchor=N)
1303
1304         names = Frame(leftwin, relief=RAISED, bd=5)
1305         Label(names, text="Local names").pack(side=TOP)
1306         ListEdit("New name: ",
1307                      self.user.localnames, None, None, names, localhelp)
1308         names.pack(fill=X, anchor=N)
1309
1310         if mode != 'novice':
1311             targwin = Frame(leftwin, relief=RAISED, bd=5)
1312             Label(targwin, text="Forwarding Options").pack(side=TOP)
1313             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1314             ListEdit("New listener:",
1315                      self.user.smtphunt, None, None, targwin, None)
1316             LabeledEntry(targwin, 'Append to MAIL FROM line:',
1317                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1318             LabeledEntry(targwin, 'Connection setup command:',
1319                      self.preconnect, '26').pack(side=TOP, fill=X)
1320             LabeledEntry(targwin, 'Connection wrapup command:',
1321                      self.postconnect, '26').pack(side=TOP, fill=X)
1322             LabeledEntry(targwin, 'Local delivery agent:',
1323                      self.mda, '26').pack(side=TOP, fill=X)
1324             LabeledEntry(targwin, 'BSMTP output file:',
1325                      self.bsmtp, '26').pack(side=TOP, fill=X)
1326             LabeledEntry(targwin, 'Listener spam-block codes:',
1327                      self.antispam, '26').pack(side=TOP, fill=X)
1328             LabeledEntry(targwin, 'Pass-through properties:',
1329                      self.properties, '26').pack(side=TOP, fill=X)
1330             Checkbutton(targwin, text="Use LMTP?",
1331                         variable=self.lmtp).pack(side=TOP, fill=X)
1332             targwin.pack(fill=X, anchor=N)
1333
1334         if mode != 'novice':
1335             leftwin.pack(side=LEFT, fill=X, anchor=N)
1336             rightwin = Frame(self)
1337         else:
1338             rightwin = self
1339
1340         optwin = Frame(rightwin, relief=RAISED, bd=5)
1341         Label(optwin, text="Processing Options").pack(side=TOP)
1342         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1343                     variable=self.keep).pack(side=TOP, anchor=W)
1344         Checkbutton(optwin, text="Fetch old messages as well as new",
1345                     variable=self.fetchall).pack(side=TOP, anchor=W)
1346         if mode != 'novice':
1347             Checkbutton(optwin, text="Flush seen messages before retrieval", 
1348                     variable=self.flush).pack(side=TOP, anchor=W)
1349             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply", 
1350                     variable=self.rewrite).pack(side=TOP, anchor=W)
1351             Checkbutton(optwin, text="Force CR/LF at end of each line",
1352                     variable=self.forcecr).pack(side=TOP, anchor=W)
1353             Checkbutton(optwin, text="Strip CR from end of each line",
1354                     variable=self.stripcr).pack(side=TOP, anchor=W)
1355             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1356                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1357             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1358                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1359             Checkbutton(optwin, text="Drop Status lines from forwarded messages", 
1360                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1361         optwin.pack(fill=X)
1362
1363         if mode != 'novice':
1364             limwin = Frame(rightwin, relief=RAISED, bd=5)
1365             Label(limwin, text="Resource Limits").pack(side=TOP)
1366             LabeledEntry(limwin, 'Message size limit:',
1367                       self.limit, '30').pack(side=TOP, fill=X)
1368             LabeledEntry(limwin, 'Size warning interval:',
1369                       self.warnings, '30').pack(side=TOP, fill=X)
1370             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1371                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1372             LabeledEntry(limwin, 'Max messages to forward per poll:',
1373                       self.batchlimit, '30').pack(side=TOP, fill=X)
1374             if self.parent.server.protocol in ('IMAP', 'IMAP-K4', 'IMAP-GSS'):
1375                 LabeledEntry(limwin, 'Interval between expunges (IMAP):',
1376                              self.expunge, '30').pack(side=TOP, fill=X)
1377             limwin.pack(fill=X)
1378
1379             if self.parent.server.protocol in ('IMAP', 'IMAP-K4', 'IMAP-GSS'):
1380                 foldwin = Frame(rightwin, relief=RAISED, bd=5)
1381                 Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1382                 ListEdit("New folder:", self.user.mailboxes,
1383                          None, None, foldwin, None)
1384                 foldwin.pack(fill=X, anchor=N)
1385
1386         if mode != 'novice':
1387             rightwin.pack(side=LEFT)
1388         else:
1389             self.pack()
1390
1391
1392 #
1393 # Top-level window that offers either novice or expert mode
1394 # (but not both at once; it disappears when one is selected).
1395 #
1396
1397 class Configurator(Frame):
1398     def __init__(self, outfile, master, onexit, parent):
1399         Frame.__init__(self, master)
1400         self.outfile = outfile
1401         self.onexit = onexit
1402         self.parent = parent
1403         self.master.title('fetchmail configurator');
1404         self.master.iconname('fetchmail configurator');
1405         Pack.config(self)
1406         self.keepalive = []     # Use this to anchor the PhotoImage object
1407         make_icon_window(self, fetchmail_gif)
1408
1409         Message(self, text="""
1410 Use `Novice Configuration' for basic fetchmail setup;
1411 with this, you can easily set up a single-drop connection
1412 to one remote mail server.
1413 """, width=600).pack(side=TOP)
1414         Button(self, text='Novice Configuration',
1415                                 fg='blue', command=self.novice).pack()
1416
1417         Message(self, text="""
1418 Use `Expert Configuration' for advanced fetchmail setup,
1419 including multiple-site or multidrop connections.
1420 """, width=600).pack(side=TOP)
1421         Button(self, text='Expert Configuration',
1422                                 fg='blue', command=self.expert).pack()
1423
1424         Message(self, text="""
1425 Or you can just select `Quit' to leave the configurator now and
1426 return to the main panel.
1427 """, width=600).pack(side=TOP)
1428         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1429
1430     def novice(self):
1431         self.master.destroy()
1432         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('novice')
1433
1434     def expert(self):
1435         self.master.destroy()
1436         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('expert')
1437
1438     def leave(self):
1439         self.master.destroy()
1440         self.onexit()
1441
1442 # Run a command an a scrolling text widget, displaying its output
1443
1444 class RunWindow(Frame):
1445     def __init__(self, command, master, parent):
1446         Frame.__init__(self, master)
1447         self.master = master
1448         self.master.title('fetchmail run window');
1449         self.master.iconname('fetchmail run window');
1450         Pack.config(self)
1451         Label(self,
1452                 text="Running "+command, 
1453                 bd=2).pack(side=TOP, pady=10)
1454         self.keepalive = []     # Use this to anchor the PhotoImage object
1455         make_icon_window(self, fetchmail_gif)
1456
1457         # This is a scrolling text window
1458         textframe = Frame(self)
1459         scroll = Scrollbar(textframe)
1460         self.textwidget = Text(textframe, setgrid=TRUE)
1461         textframe.pack(side=TOP, expand=YES, fill=BOTH)
1462         self.textwidget.config(yscrollcommand=scroll.set)
1463         self.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
1464         scroll.config(command=self.textwidget.yview)
1465         scroll.pack(side=RIGHT, fill=BOTH)
1466         textframe.pack(side=TOP)
1467
1468         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1469
1470         self.update()   # Draw widget before executing fetchmail
1471
1472         child_stdout = os.popen(command + " 2>&1", "r")
1473         while 1:
1474             ch = child_stdout.read(1)
1475             if not ch:
1476                 break
1477             self.textwidget.insert(END, ch)
1478         self.textwidget.insert(END, "Done.")
1479         self.textwidget.see(END);
1480
1481     def leave(self):
1482         Widget.destroy(self.master)
1483
1484 # Here's where we choose either configuration or launching
1485
1486 class MainWindow(Frame):
1487     def __init__(self, outfile, master=None):
1488         Frame.__init__(self, master)
1489         self.outfile = outfile
1490         self.master.title('fetchmail launcher');
1491         self.master.iconname('fetchmail launcher');
1492         Pack.config(self)
1493         Label(self,
1494                 text='Fetchmailconf ' + version, 
1495                 bd=2).pack(side=TOP, pady=10)
1496         self.keepalive = []     # Use this to anchor the PhotoImage object
1497         make_icon_window(self, fetchmail_gif)
1498         self.debug = 0
1499
1500         Message(self, text="""
1501 Use `Configure fetchmail' to tell fetchmail about the remote
1502 servers it should poll (the host name, your username there,
1503 whether to use POP or IMAP, and so forth).
1504 """, width=600).pack(side=TOP)
1505         self.configbutton = Button(self, text='Configure fetchmail',
1506                                 fg='blue', command=self.configure)
1507         self.configbutton.pack()
1508
1509         Message(self, text="""
1510 Use `Test fetchmail' to run fetchmail with debugging enabled.
1511 This is a good way to test out a new configuration.
1512 """, width=600).pack(side=TOP)
1513         Button(self, text='Test fetchmail',fg='blue', command=self.test).pack()
1514
1515         Message(self, text="""
1516 Use `Run fetchmail' to run fetchmail in foreground.
1517 Progress  messages will be shown, but not debug messages.
1518 """, width=600).pack(side=TOP)
1519         Button(self, text='Run fetchmail', fg='blue', command=self.run).pack()
1520
1521         Message(self, text="""
1522 Or you can just select `Quit' to exit the launcher now.
1523 """, width=600).pack(side=TOP)
1524         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1525
1526     def configure(self):
1527         self.configbutton.configure(state=DISABLED)
1528         Configurator(self.outfile, Toplevel(),
1529                      lambda self=self: self.configbutton.configure(state=NORMAL),
1530                      self)
1531
1532     def test(self):
1533         RunWindow("fetchmail -d0 -v --nosyslog", Toplevel(), self)
1534
1535     def run(self):
1536         RunWindow("fetchmail -d0", Toplevel(), self)
1537
1538     def leave(self):
1539         self.quit()
1540
1541 # Functions for turning a dictionary into an instantiated object tree.
1542
1543 def intersect(list1, list2):
1544 # Compute set intersection of lists
1545     res = []
1546     for x in list1:
1547         if x in list2:
1548             res.append(x)
1549     return res
1550
1551 def setdiff(list1, list2):
1552 # Compute set difference of lists
1553     res = []
1554     for x in list1:
1555         if not x in list2:
1556             res.append(x)
1557     return res
1558
1559 def copy_instance(toclass, fromdict):
1560 # Initialize a class object of given type from a conformant dictionary.
1561     for fld in fromdict.keys():
1562         if not fld in dictmembers:
1563             dictmembers.append(fld)
1564 # The `optional' fields are the ones we can ignore for purposes of
1565 # conformability checking; they'll still get copied if they are
1566 # present in the dictionary.
1567     optional = ('interface', 'monitor', 'netsec')
1568     class_sig = setdiff(toclass.__dict__.keys(), optional)
1569     class_sig.sort()
1570     dict_keys = setdiff(fromdict.keys(), optional)
1571     dict_keys.sort()
1572     common = intersect(class_sig, dict_keys)
1573     if 'typemap' in class_sig: 
1574         class_sig.remove('typemap')
1575     if tuple(class_sig) != tuple(dict_keys):
1576         print "Fields don't match what fetchmailconf expected:"
1577 #       print "Class signature: " + `class_sig`
1578 #       print "Dictionary keys: " + `dict_keys`
1579         diff = setdiff(class_sig, common)
1580         if diff:
1581             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
1582         diff = setdiff(dict_keys, common)
1583         if diff:
1584             print "Not matched in dictionary keys: " + `diff`
1585         sys.exit(1)
1586     else:
1587         for x in fromdict.keys():
1588             setattr(toclass, x, fromdict[x])
1589
1590 #
1591 # And this is the main sequence.  How it works:  
1592 #
1593 # First, call `fetchmail --configdump' and trap the output in a tempfile.
1594 # This should fill it with a Python initializer for a variable `fetchmailrc'.
1595 # Run execfile on the file to pull fetchmailrc into Python global space.
1596 # You don't want static data, though; you want, instead, a tree of objects
1597 # with the same data members and added appropriate methods.
1598 #
1599 # This is what the copy_instance function() is for.  It tries to copy a
1600 # dictionary field by field into a class, aborting if the class and dictionary
1601 # have different data members (except for any typemap member in the class;
1602 # that one is strictly for use by the MyWidget supperclass).
1603 #
1604 # Once the object tree is set up, require user to choose novice or expert
1605 # mode and instantiate an edit object for the configuration.  Class methods
1606 # will take it all from there.
1607 #
1608 # Options (not documented because they're for fetchmailconf debuggers only):
1609 # -d: Read the configuration and dump it to stdout before editing.  Dump
1610 #     the edited result to stdout as well.
1611 # -f: specify the run control file to read.
1612
1613 if __name__ == '__main__': 
1614
1615     fetchmail_gif = """
1616 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
1617 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
1618 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
1619 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
1620 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
1621 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
1622 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
1623 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
1624 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
1625 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
1626 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
1627 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
1628 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
1629 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
1630 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
1631 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
1632 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
1633 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
1634 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
1635 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
1636 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
1637 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
1638 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
1639 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
1640 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
1641 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
1642 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
1643 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
1644 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
1645 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
1646 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
1647 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
1648 """
1649 # Note on making icons: the above was generated by the following procedure:
1650 #
1651 # import base64
1652 # data = open("fetchmail.gif", "rb").read()
1653 # print "fetchmail_gif =\\"
1654 # print repr(base64.encodestring(data))
1655 #
1656
1657     # Process options
1658     (options, arguments) = getopt.getopt(sys.argv[1:], "df:")
1659     dump = rcfile = None;
1660     for (switch, val) in options:
1661         if (switch == '-d'):
1662             dump = TRUE
1663         elif (switch == '-f'):
1664             rcfile = val
1665
1666     # Get client host's FQDN
1667     hostname = socket.gethostbyaddr(socket.gethostname())[0]
1668
1669     # Compute defaults
1670     ConfigurationDefaults = Configuration()
1671     ServerDefaults = Server()
1672     UserDefaults = User()
1673
1674     # Read the existing configuration
1675     tmpfile = "/tmp/fetchmailconf." + `os.getpid()`
1676     if rcfile:
1677         cmd = "fetchmail -f " + rcfile + " --configdump --nosyslog >" + tmpfile
1678     else:
1679         cmd = "fetchmail --configdump --nosyslog >" + tmpfile
1680         
1681     try:
1682         s = os.system(cmd)
1683         if s != 0:
1684             print "`" + cmd + "' run failure, status " + `s`
1685             raise SystemExit
1686     except:
1687         print "Unknown error while running fetchmail --configdump"
1688         os.remove(tmpfile)
1689         sys.exit(1)
1690
1691     try:
1692         execfile(tmpfile)
1693     except:
1694         print "Can't read configuration output of fetchmail --configdump."
1695         os.remove(tmpfile)
1696         sys.exit(1)
1697         
1698     os.remove(tmpfile)
1699
1700     # The tricky part -- initializing objects from the configuration global
1701     # `Configuration' is the top level of the object tree we're going to mung.
1702     # The dictmembers list is used to track the set of fields the dictionary
1703     # contains; in particular, we can use it to tell whether things like the
1704     # monitor, interface, and netsec fields are present.
1705     dictmembers = []
1706     Fetchmailrc = Configuration()
1707     copy_instance(Fetchmailrc, fetchmailrc)
1708     Fetchmailrc.servers = [];
1709     for server in fetchmailrc['servers']:
1710         Newsite = Server()
1711         copy_instance(Newsite, server)
1712         Fetchmailrc.servers.append(Newsite)
1713         Newsite.users = [];
1714         for user in server['users']:
1715             Newuser = User()
1716             copy_instance(Newuser, user)
1717             Newsite.users.append(Newuser)
1718
1719     # We may want to display the configuration and quit
1720     if dump:
1721         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
1722
1723     # The theory here is that -f alone sets the rcfile location,
1724     # but -d and -f together mean the new configuration should go to stdout.
1725     if not rcfile and not dump:
1726         rcfile = os.environ["HOME"] + "/.fetchmailrc"
1727
1728     # OK, now run the configuration edit
1729     root = MainWindow(rcfile)
1730     root.mainloop()
1731
1732 # The following sets edit modes for GNU EMACS
1733 # Local Variables:
1734 # mode:python
1735 # End: