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