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