]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf.py
Remove bogus Netscape IMAP4rev1 Service >= 3.6 warning about BODY[TEXT]
[~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.51 $Revision$"
9
10 from Tkinter import *
11 from Dialog import *
12 import sys, time, os, string, socket, getopt, tempfile
13
14 #
15 # Define the data structures the GUIs will be tossing around
16 #
17 class Configuration:
18     def __init__(self):
19         self.poll_interval = 0          # Normally, run in foreground
20         self.logfile = None             # No logfile, initially
21         self.idfile = os.environ["HOME"] + "/.fetchids"  # Default idfile, initially
22         self.postmaster = None          # No last-resort address, initially
23         self.bouncemail = TRUE          # Bounce errors to users
24         self.spambounce = FALSE         # Bounce spam errors
25         self.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.service and 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, "InterChange") > 0:
1441                 warnings = warnings + """
1442
1443 The InterChange IMAP server at release levels below 3.61.08 screws up
1444 on mail with attachments.  It doesn't fetch them if you give it a
1445 BODY[TEXT] request, though it does if you request RFC822.TEXT.
1446 According to the IMAP RFCs and their maintainer these should be
1447 equivalent -- and we can't drop the BODY[TEXT] form because M$
1448 Exchange (quite legally under RFC2062) rejectsit.  The InterChange
1449 folks claim to have fixed this bug in 3.61.08.
1450
1451 """
1452             if string.find(greetline, "Imail") > 0:
1453                 warnings = warnings + """
1454 We've seen a bug report indicating that this IMAP server (at least as of
1455 version 5.0.7) returns an invalid body size for messages with MIME
1456 attachments; the effect is to drop the attachments on the floor.  We
1457 recommend you upgrade to a non-broken IMAP server.
1458
1459 """
1460             if string.find(greetline, "Domino IMAP4") > 0:
1461                 warnings = warnings + """
1462 Your IMAP server appears to be Lotus Domino.  This server, at least up
1463 to version 4.6.2a, has a bug in its generation of MIME boundaries (see
1464 the details in the fetchmail FAQ).  As a result, even MIME aware MUAs
1465 will see attachments as part of the message text.  If your Domino server's
1466 POP3 facility is enabled, we recommend you fall back on it.
1467
1468 """
1469
1470 ### Checks for protocol variants start here
1471
1472             closebrak = string.find(greetline, ">")
1473             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1474                 warnings = warnings + """
1475 It looks like you could use APOP on this server and avoid sending it your
1476 password in clear.  You should talk to the mailserver administrator about
1477 this.
1478
1479 """
1480             if string.find(greetline, "IMAP2bis") > 0:
1481                 warnings = warnings + """
1482 IMAP2bis servers have a minor problem; they can't peek at messages without
1483 marking them seen.  If you take a line hit during the retrieval, the
1484 interrupted message may get left on the server, marked seen.
1485
1486 To work around this, it is recommended that you set the `fetchall'
1487 option on all user entries associated with this server, so any stuck
1488 mail will be retrieved next time around.
1489
1490 To fix this bug, upgrade to an IMAP4 server.  The fetchmail FAQ includes
1491 a pointer to an open-source implementation.
1492
1493 """
1494             if string.find(greetline, "IMAP4rev1") > 0:
1495                 warnings = warnings + """
1496 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1497 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1498 has therefore been extremely well tested with this class of server.
1499
1500 """
1501             if warnings == '':
1502                 warnings = warnings + """
1503 Fetchmail doesn't know anything special about this server type.
1504
1505 """
1506
1507             # Display success window with warnings
1508             title = "Autoprobe of " + realhost + " succeeded"
1509             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1510             self.protocol.set(protocol)
1511             self.service.set(defaultports[protocol])
1512         confwin.title(title)
1513         confwin.iconname(title)
1514         Label(confwin, text=title).pack()
1515         Message(confwin, text=confirm, width=600).pack()
1516         Button(confwin, text='Done',
1517                    command=lambda x=confwin: x.destroy(), bd=2).pack()
1518
1519 #
1520 # User editing stuff
1521 #
1522
1523 userhelp = {
1524     'title' : 'User option help',
1525     'banner': 'User options',
1526     'text' : """
1527 You may use this panel to set options
1528 that may differ between individual
1529 users on your site.
1530
1531 Once you have a user configuration set
1532 up as you like it, you can select `OK' to
1533 store it in the user list maintained in
1534 the site configuration window.
1535
1536 If you wish to discard the changes you have
1537 made to user options, select `Quit'.
1538 """}
1539
1540 localhelp = {
1541     'title' : 'Local name help',
1542     'banner': 'Local names',
1543     'text' : """
1544 The local name(s) in a user entry are the
1545 people on the client machine who should
1546 receive mail from the poll described.
1547
1548 Note: if a user entry has more than one
1549 local name, messages will be retrieved
1550 in multidrop mode.  This complicates
1551 the configuration issues; see the manual
1552 page section on multidrop mode.
1553
1554 Warning: Be careful with local names
1555 such as foo@bar.com, as that can cause
1556 the mail to be sent to foo@bar.com instead
1557 of sending it to your local system.
1558 """}
1559
1560 class UserEdit(Frame, MyWidget):
1561     def __init__(self, username, parent):
1562         self.parent = parent
1563         self.user = None
1564         for user in parent.server.users:
1565             if user.remote == username:
1566                 self.user = user
1567         if self.user == None:
1568             self.user = User()
1569             self.user.remote = username
1570             self.user.localnames = [username]
1571             parent.server.users.append(self.user)
1572
1573     def edit(self, mode, master=None):
1574         Frame.__init__(self, master)
1575         Pack.config(self)
1576         self.master.title('Fetchmail user ' + self.user.remote
1577                           + ' querying ' + self.parent.server.pollname);
1578         self.master.iconname('Fetchmail user ' + self.user.remote);
1579         self.post(User, 'user')
1580         self.makeWidgets(mode, self.parent.server.pollname)
1581         self.keepalive = []     # Use this to anchor the PhotoImage object
1582         make_icon_window(self, fetchmail_icon)
1583 #       self.grab_set()
1584 #       self.focus_set()
1585 #       self.wait_window()
1586         return self
1587
1588     def destruct(self):
1589         # Yes, this test can fail -- if you delete the parent window.
1590         if self.parent.subwidgets.has_key(self.user.remote):
1591             del self.parent.subwidgets[self.user.remote]
1592         self.master.destroy()
1593
1594     def nosave(self):
1595         if ConfirmQuit(self, 'user option editing'):
1596             self.destruct()
1597
1598     def save(self):
1599         ok = 0
1600         for x in self.user.localnames: ok = ok + (string.find(x, '@') != -1)
1601         if ok == 0 or  Dialog(self,
1602             title = "Really accept an embedded '@' ?",
1603             text = "Local names with an embedded '@', such as in foo@bar "
1604                    "might result in your mail being sent to foo@bar.com "
1605                    "instead of your local system.\n Are you sure you want "
1606                    "a local user name with an '@' in it?",
1607             bitmap = 'question',
1608             strings = ('Yes', 'No'),
1609             default = 1).num == 0:
1610                 self.fetch(User, 'user')
1611                 self.destruct()
1612
1613     def makeWidgets(self, mode, servername):
1614         dispose_window(self,
1615                         "User options for " + self.user.remote + " querying " + servername,
1616                         userhelp)
1617
1618         if mode != 'novice':
1619             leftwin = Frame(self);
1620         else:
1621             leftwin = self
1622
1623         secwin = Frame(leftwin, relief=RAISED, bd=5)
1624         Label(secwin, text="Authentication").pack(side=TOP)
1625         LabeledEntry(secwin, 'Password:',
1626                       self.password, '12').pack(side=TOP, fill=X)
1627         secwin.pack(fill=X, anchor=N)
1628
1629         if 'ssl' in feature_options or 'ssl' in dictmembers:
1630             sslwin = Frame(leftwin, relief=RAISED, bd=5)
1631             Checkbutton(sslwin, text="Use SSL?",
1632                         variable=self.ssl).pack(side=TOP, fill=X)
1633             LabeledEntry(sslwin, 'SSL key:',
1634                          self.sslkey, '14').pack(side=TOP, fill=X)
1635             LabeledEntry(sslwin, 'SSL certificate:',
1636                          self.sslcert, '14').pack(side=TOP, fill=X)
1637             Checkbutton(sslwin, text="Check server SSL certificate?",
1638                         variable=self.sslcertck).pack(side=TOP, fill=X)
1639             LabeledEntry(sslwin, 'SSL trusted certificate directory:',
1640                          self.sslcertpath, '14').pack(side=TOP, fill=X)
1641             LabeledEntry(sslwin, 'SSL key fingerprint:',
1642                          self.sslfingerprint, '14').pack(side=TOP, fill=X)
1643             sslwin.pack(fill=X, anchor=N)
1644
1645         names = Frame(leftwin, relief=RAISED, bd=5)
1646         Label(names, text="Local names").pack(side=TOP)
1647         ListEdit("New name: ",
1648                      self.user.localnames, None, None, names, localhelp)
1649         names.pack(fill=X, anchor=N)
1650
1651         if mode != 'novice':
1652             targwin = Frame(leftwin, relief=RAISED, bd=5)
1653             Label(targwin, text="Forwarding Options").pack(side=TOP)
1654             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1655             ListEdit("New listener:",
1656                      self.user.smtphunt, None, None, targwin, None)
1657             Label(targwin, text="Domains to fetch from (ODMR/ETRN only)").pack(side=TOP)
1658             ListEdit("Domains:",
1659                      self.user.fetchdomains, None, None, targwin, None)
1660             LabeledEntry(targwin, 'Append to MAIL FROM line:',
1661                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1662             LabeledEntry(targwin, 'Set RCPT To address:',
1663                      self.smtpname, '26').pack(side=TOP, fill=X)
1664             LabeledEntry(targwin, 'Connection setup command:',
1665                      self.preconnect, '26').pack(side=TOP, fill=X)
1666             LabeledEntry(targwin, 'Connection wrapup command:',
1667                      self.postconnect, '26').pack(side=TOP, fill=X)
1668             LabeledEntry(targwin, 'Local delivery agent:',
1669                      self.mda, '26').pack(side=TOP, fill=X)
1670             LabeledEntry(targwin, 'BSMTP output file:',
1671                      self.bsmtp, '26').pack(side=TOP, fill=X)
1672             LabeledEntry(targwin, 'Listener spam-block codes:',
1673                      self.antispam, '26').pack(side=TOP, fill=X)
1674             LabeledEntry(targwin, 'Pass-through properties:',
1675                      self.properties, '26').pack(side=TOP, fill=X)
1676             Checkbutton(targwin, text="Use LMTP?",
1677                         variable=self.lmtp).pack(side=TOP, fill=X)
1678             targwin.pack(fill=X, anchor=N)
1679
1680         if mode != 'novice':
1681             leftwin.pack(side=LEFT, fill=X, anchor=N)
1682             rightwin = Frame(self)
1683         else:
1684             rightwin = self
1685
1686         optwin = Frame(rightwin, relief=RAISED, bd=5)
1687         Label(optwin, text="Processing Options").pack(side=TOP)
1688         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1689                     variable=self.keep).pack(side=TOP, anchor=W)
1690         Checkbutton(optwin, text="Fetch old messages as well as new",
1691                     variable=self.fetchall).pack(side=TOP, anchor=W)
1692         if mode != 'novice':
1693             Checkbutton(optwin, text="Flush seen messages before retrieval",
1694                     variable=self.flush).pack(side=TOP, anchor=W)
1695             Checkbutton(optwin, text="Flush oversized messages before retrieval",
1696                     variable=self.limitflush).pack(side=TOP, anchor=W)
1697             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply",
1698                     variable=self.rewrite).pack(side=TOP, anchor=W)
1699             Checkbutton(optwin, text="Force CR/LF at end of each line",
1700                     variable=self.forcecr).pack(side=TOP, anchor=W)
1701             Checkbutton(optwin, text="Strip CR from end of each line",
1702                     variable=self.stripcr).pack(side=TOP, anchor=W)
1703             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1704                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1705             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1706                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1707             Checkbutton(optwin, text="Drop Status lines from forwarded messages",
1708                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1709             Checkbutton(optwin, text="Drop Delivered-To lines from forwarded messages",
1710                     variable=self.dropdelivered).pack(side=TOP, anchor=W)
1711         optwin.pack(fill=X)
1712
1713         if mode != 'novice':
1714             limwin = Frame(rightwin, relief=RAISED, bd=5)
1715             Label(limwin, text="Resource Limits").pack(side=TOP)
1716             LabeledEntry(limwin, 'Message size limit:',
1717                       self.limit, '30').pack(side=TOP, fill=X)
1718             LabeledEntry(limwin, 'Size warning interval:',
1719                       self.warnings, '30').pack(side=TOP, fill=X)
1720             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1721                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1722             LabeledEntry(limwin, 'Max message sizes to fetch per transaction:',
1723                       self.fetchsizelimit, '30').pack(side=TOP, fill=X)
1724             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1725                 LabeledEntry(limwin, 'Use fast UIDL:',
1726                         self.fastuidl, '30').pack(side=TOP, fill=X)
1727             LabeledEntry(limwin, 'Max messages to forward per poll:',
1728                       self.batchlimit, '30').pack(side=TOP, fill=X)
1729             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1730                 LabeledEntry(limwin, 'Interval between expunges:',
1731                              self.expunge, '30').pack(side=TOP, fill=X)
1732             Checkbutton(limwin, text="Idle after each poll (IMAP only)",
1733                     variable=self.idle).pack(side=TOP, anchor=W)
1734             limwin.pack(fill=X)
1735
1736             if self.parent.server.protocol == 'IMAP':
1737                 foldwin = Frame(rightwin, relief=RAISED, bd=5)
1738                 Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1739                 ListEdit("New folder:", self.user.mailboxes,
1740                          None, None, foldwin, None)
1741                 foldwin.pack(fill=X, anchor=N)
1742
1743         if mode != 'novice':
1744             rightwin.pack(side=LEFT)
1745         else:
1746             self.pack()
1747
1748
1749 #
1750 # Top-level window that offers either novice or expert mode
1751 # (but not both at once; it disappears when one is selected).
1752 #
1753
1754 class Configurator(Frame):
1755     def __init__(self, outfile, master, onexit, parent):
1756         Frame.__init__(self, master)
1757         self.outfile = outfile
1758         self.onexit = onexit
1759         self.parent = parent
1760         self.master.title('fetchmail configurator');
1761         self.master.iconname('fetchmail configurator');
1762         Pack.config(self)
1763         self.keepalive = []     # Use this to anchor the PhotoImage object
1764         make_icon_window(self, fetchmail_icon)
1765
1766         Message(self, text="""
1767 Use `Novice Configuration' for basic fetchmail setup;
1768 with this, you can easily set up a single-drop connection
1769 to one remote mail server.
1770 """, width=600).pack(side=TOP)
1771         Button(self, text='Novice Configuration',
1772                                 fg='blue', command=self.novice).pack()
1773
1774         Message(self, text="""
1775 Use `Expert Configuration' for advanced fetchmail setup,
1776 including multiple-site or multidrop connections.
1777 """, width=600).pack(side=TOP)
1778         Button(self, text='Expert Configuration',
1779                                 fg='blue', command=self.expert).pack()
1780
1781         Message(self, text="""
1782 Or you can just select `Quit' to leave the configurator now and
1783 return to the main panel.
1784 """, width=600).pack(side=TOP)
1785         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1786         master.protocol("WM_DELETE_WINDOW", self.leave)
1787
1788     def novice(self):
1789         self.master.destroy()
1790         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('novice')
1791
1792     def expert(self):
1793         self.master.destroy()
1794         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('expert')
1795
1796     def leave(self):
1797         self.master.destroy()
1798         self.onexit()
1799
1800 # Run a command in a scrolling text widget, displaying its output
1801
1802 class RunWindow(Frame):
1803     def __init__(self, command, master, parent):
1804         Frame.__init__(self, master)
1805         self.master = master
1806         self.master.title('fetchmail run window');
1807         self.master.iconname('fetchmail run window');
1808         Pack.config(self)
1809         Label(self,
1810                 text="Running "+command,
1811                 bd=2).pack(side=TOP, pady=10)
1812         self.keepalive = []     # Use this to anchor the PhotoImage object
1813         make_icon_window(self, fetchmail_icon)
1814
1815         # This is a scrolling text window
1816         textframe = Frame(self)
1817         scroll = Scrollbar(textframe)
1818         self.textwidget = Text(textframe, setgrid=TRUE)
1819         textframe.pack(side=TOP, expand=YES, fill=BOTH)
1820         self.textwidget.config(yscrollcommand=scroll.set)
1821         self.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
1822         scroll.config(command=self.textwidget.yview)
1823         scroll.pack(side=RIGHT, fill=BOTH)
1824         textframe.pack(side=TOP)
1825
1826         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1827
1828         self.update()   # Draw widget before executing fetchmail
1829
1830         # Always look for a runnable command in the directory we're running in
1831         # first. This avoids some obscure version-skew errors that can occur
1832         # if you pick up an old fetchmail from the standard system locations.
1833         os.environ["PATH"] = os.path.dirname(sys.argv[0]) + ":" + os.environ["PATH"]
1834         child_stdout = os.popen(command + " 2>&1 </dev/null", "r")
1835         while 1:
1836             ch = child_stdout.read(1)
1837             if not ch:
1838                 break
1839             self.textwidget.insert(END, ch)
1840         self.textwidget.insert(END, "Done.")
1841         self.textwidget.see(END);
1842
1843     def leave(self):
1844         self.master.destroy()
1845
1846 # Here's where we choose either configuration or launching
1847
1848 class MainWindow(Frame):
1849     def __init__(self, outfile, master=None):
1850         Frame.__init__(self, master)
1851         self.outfile = outfile
1852         self.master.title('fetchmail launcher');
1853         self.master.iconname('fetchmail launcher');
1854         Pack.config(self)
1855         Label(self,
1856                 text='Fetchmailconf ' + version,
1857                 bd=2).pack(side=TOP, pady=10)
1858         self.keepalive = []     # Use this to anchor the PhotoImage object
1859         make_icon_window(self, fetchmail_icon)
1860         self.debug = 0
1861
1862         ## Test icon display with the following:
1863         # icon_image = PhotoImage(data=fetchmail_icon)
1864         # Label(self, image=icon_image).pack(side=TOP, pady=10)
1865         # self.keepalive.append(icon_image)
1866
1867         Message(self, text="""
1868 Use `Configure fetchmail' to tell fetchmail about the remote
1869 servers it should poll (the host name, your username there,
1870 whether to use POP or IMAP, and so forth).
1871 """, width=600).pack(side=TOP)
1872         self.configbutton = Button(self, text='Configure fetchmail',
1873                                 fg='blue', command=self.configure)
1874         self.configbutton.pack()
1875
1876         Message(self, text="""
1877 Use `Run fetchmail' to run fetchmail with debugging enabled.
1878 This is a good way to test out a new configuration.
1879 """, width=600).pack(side=TOP)
1880         Button(self, text='Run fetchmail',fg='blue', command=self.test).pack()
1881
1882         Message(self, text="""
1883 Use `Run fetchmail' to run fetchmail in foreground.
1884 Progress  messages will be shown, but not debug messages.
1885 """, width=600).pack(side=TOP)
1886         Button(self, text='Run fetchmail', fg='blue', command=self.run).pack()
1887
1888         Message(self, text="""
1889 Or you can just select `Quit' to exit the launcher now.
1890 """, width=600).pack(side=TOP)
1891         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1892
1893     def configure(self):
1894         self.configbutton.configure(state=DISABLED)
1895         Configurator(self.outfile, Toplevel(),
1896                      lambda self=self: self.configbutton.configure(state=NORMAL),
1897                      self)
1898     def test(self):
1899         cmd = "fetchmail -N -d0 --nosyslog -v"
1900         if rcfile:
1901             cmd = cmd + " -f " + rcfile
1902         RunWindow(cmd, Toplevel(), self)
1903
1904     def run(self):
1905         cmd = "fetchmail -N -d0"
1906         if rcfile:
1907             cmd = cmd + " -f " + rcfile
1908         RunWindow(cmd, Toplevel(), self)
1909
1910     def leave(self):
1911         self.quit()
1912
1913 # Functions for turning a dictionary into an instantiated object tree.
1914
1915 def intersect(list1, list2):
1916 # Compute set intersection of lists
1917     res = []
1918     for x in list1:
1919         if x in list2:
1920             res.append(x)
1921     return res
1922
1923 def setdiff(list1, list2):
1924 # Compute set difference of lists
1925     res = []
1926     for x in list1:
1927         if not x in list2:
1928             res.append(x)
1929     return res
1930
1931 def copy_instance(toclass, fromdict):
1932 # Initialize a class object of given type from a conformant dictionary.
1933     for fld in fromdict.keys():
1934         if not fld in dictmembers:
1935             dictmembers.append(fld)
1936 # The `optional' fields are the ones we can ignore for purposes of
1937 # conformability checking; they'll still get copied if they are
1938 # present in the dictionary.
1939     optional = ('interface', 'monitor',
1940                 'esmtpname', 'esmtppassword',
1941                 'ssl', 'sslkey', 'sslcert', 'sslproto', 'sslcertck',
1942                 'sslcertpath', 'sslfingerprint', 'showdots')
1943     class_sig = setdiff(toclass.__dict__.keys(), optional)
1944     class_sig.sort()
1945     dict_keys = setdiff(fromdict.keys(), optional)
1946     dict_keys.sort()
1947     common = intersect(class_sig, dict_keys)
1948     if 'typemap' in class_sig:
1949         class_sig.remove('typemap')
1950     if tuple(class_sig) != tuple(dict_keys):
1951         print "Fields don't match what fetchmailconf expected:"
1952 #       print "Class signature: " + `class_sig`
1953 #       print "Dictionary keys: " + `dict_keys`
1954         diff = setdiff(class_sig, common)
1955         if diff:
1956             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
1957         diff = setdiff(dict_keys, common)
1958         if diff:
1959             print "Not matched in dictionary keys: " + `diff`
1960         sys.exit(1)
1961     else:
1962         for x in fromdict.keys():
1963             setattr(toclass, x, fromdict[x])
1964
1965 #
1966 # And this is the main sequence.  How it works:
1967 #
1968 # First, call `fetchmail --configdump' and trap the output in a tempfile.
1969 # This should fill it with a Python initializer for a variable `fetchmailrc'.
1970 # Run execfile on the file to pull fetchmailrc into Python global space.
1971 # You don't want static data, though; you want, instead, a tree of objects
1972 # with the same data members and added appropriate methods.
1973 #
1974 # This is what the copy_instance function() is for.  It tries to copy a
1975 # dictionary field by field into a class, aborting if the class and dictionary
1976 # have different data members (except for any typemap member in the class;
1977 # that one is strictly for use by the MyWidget supperclass).
1978 #
1979 # Once the object tree is set up, require user to choose novice or expert
1980 # mode and instantiate an edit object for the configuration.  Class methods
1981 # will take it all from there.
1982 #
1983 # Options (not documented because they're for fetchmailconf debuggers only):
1984 # -d: Read the configuration and dump it to stdout before editing.  Dump
1985 #     the edited result to stdout as well.
1986 # -f: specify the run control file to read.
1987
1988 if __name__ == '__main__':
1989
1990     if not os.environ.has_key("DISPLAY"):
1991         print "fetchmailconf must be run under X"
1992         sys.exit(1)
1993
1994     fetchmail_icon = """
1995 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
1996 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
1997 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
1998 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
1999 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
2000 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
2001 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
2002 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
2003 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
2004 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
2005 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
2006 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
2007 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
2008 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
2009 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
2010 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
2011 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
2012 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
2013 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
2014 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
2015 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
2016 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
2017 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
2018 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
2019 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
2020 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
2021 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
2022 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
2023 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
2024 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
2025 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
2026 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
2027 """
2028 # The base64 data in the string above was generated by the following procedure:
2029 #
2030 # import base64
2031 # print base64.encodestring(open("fetchmail.gif", "rb").read())
2032 #
2033
2034     # Process options
2035     (options, arguments) = getopt.getopt(sys.argv[1:], "df:hV")
2036     dump = rcfile = None;
2037     for (switch, val) in options:
2038         if (switch == '-d'):
2039             dump = TRUE
2040         elif (switch == '-f'):
2041             rcfile = val
2042         elif (switch == '-h'):
2043             print """
2044 Usage: fetchmailconf {[-d] [-f fetchmailrc]|-h|-V}
2045 -d       - dump configuration (for debugging)
2046 -f fmrc  - read alternate fetchmailrc file
2047 -h       - print this help text and quit
2048 -V       - print fetchmailconf version and quit
2049 """
2050             sys.exit(0)
2051         elif (switch == '-V'):
2052             print "fetchmailconf %s" % version
2053             sys.exit(0)
2054
2055     # Get client host's FQDN
2056     hostname = socket.gethostbyaddr(socket.gethostname())[0]
2057
2058     # Compute defaults
2059     ConfigurationDefaults = Configuration()
2060     ServerDefaults = Server()
2061     UserDefaults = User()
2062
2063     # Read the existing configuration.  We set the umask to 077 to make sure
2064     # that group & other read/write permissions are shut off -- we wouldn't
2065     # want crackers to snoop password information out of the tempfile.
2066     tmpfile = tempfile.mktemp()
2067     if rcfile:
2068         cmd = "umask 077 && fetchmail </dev/null -f " + rcfile + " --configdump --nosyslog >" + tmpfile
2069     else:
2070         cmd = "umask 077 && fetchmail </dev/null --configdump --nosyslog >" + tmpfile
2071
2072     try:
2073         s = os.system(cmd)
2074         if s != 0:
2075             print "`" + cmd + "' run failure, status " + `s`
2076             raise SystemExit
2077     except:
2078         print "Unknown error while running fetchmail --configdump"
2079         os.remove(tmpfile)
2080         sys.exit(1)
2081
2082     try:
2083         execfile(tmpfile)
2084     except:
2085         print "Can't read configuration output of fetchmail --configdump."
2086         os.remove(tmpfile)
2087         sys.exit(1)
2088
2089     os.remove(tmpfile)
2090
2091     # The tricky part -- initializing objects from the configuration global
2092     # `Configuration' is the top level of the object tree we're going to mung.
2093     # The dictmembers list is used to track the set of fields the dictionary
2094     # contains; in particular, we can use it to tell whether things like the
2095     # monitor, interface, ssl, sslkey, or sslcert fields are present.
2096     dictmembers = []
2097     Fetchmailrc = Configuration()
2098     copy_instance(Fetchmailrc, fetchmailrc)
2099     Fetchmailrc.servers = [];
2100     for server in fetchmailrc['servers']:
2101         Newsite = Server()
2102         copy_instance(Newsite, server)
2103         Fetchmailrc.servers.append(Newsite)
2104         Newsite.users = [];
2105         for user in server['users']:
2106             Newuser = User()
2107             copy_instance(Newuser, user)
2108             Newsite.users.append(Newuser)
2109
2110     # We may want to display the configuration and quit
2111     if dump:
2112         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
2113
2114     # The theory here is that -f alone sets the rcfile location,
2115     # but -d and -f together mean the new configuration should go to stdout.
2116     if not rcfile and not dump:
2117         rcfile = os.environ["HOME"] + "/.fetchmailrc"
2118
2119     # OK, now run the configuration edit
2120     root = MainWindow(rcfile)
2121     root.mainloop()
2122
2123 # The following sets edit modes for GNU EMACS
2124 # Local Variables:
2125 # mode:python
2126 # End: