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