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