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