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