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