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