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