]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf.py
Lower fastuidl default to 4 (was 10). Patch by Sunil Shetye.
[~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.52 $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 = 4       # Do fast uidl 3 out of 4 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 poll interval in seconds.
670    If 0, fetchmail will run in the foreground once when started.
671    If > 0, fetchmail will run in the background and start a new poll
672    cycle after the interval has elapsed.
673
674 6. Options to fetch old messages as well as new, or to suppress
675    deletion of fetched message.
676
677 The novice-configuration code will assume that you want to forward mail
678 to a local sendmail listener with no special options.
679 """}
680
681 configure_expert_help = {
682     'title' : 'Fetchmail expert configurator help',
683     'banner': 'Expert configurator help',
684     'text' : """
685 In the `Expert Configurator Controls' panel, you can:
686
687 Press `Save' to save the new fetchmail configuration you have edited.
688 Press `Quit' to exit without saving.
689 Press `Help' to bring up this help message.
690
691 In the `Run Controls' panel, you can set the following options that
692 control how fetchmail runs:
693
694 Poll interval
695         Number of seconds to wait between polls in the background.
696         If zero, fetchmail will run in foreground.
697
698 Logfile
699         If empty, emit progress and error messages to stderr.
700         Otherwise this gives the name of the files to write to.
701         This field is ignored if the "Log to syslog?" option is on.
702
703 Idfile
704         If empty, store seen-message IDs in .fetchids under user's home
705         directory.  If nonempty, use given file name.
706
707 Postmaster
708         Who to send multidrop mail to as a last resort if no address can
709         be matched.  Normally empty; in this case, fetchmail treats the
710         invoking user as the address of last resort unless that user is
711         root.  If that user is root, fetchmail sends to `postmaster'.
712
713 Bounces to sender?
714         If this option is on (the default) error mail goes to the sender.
715         Otherwise it goes to the postmaster.
716
717 Send spam bounces?
718         If this option is on, spam bounces are sent to the sender or
719         postmaster (depending on the "Bounces to sender?" option.  Otherwise,
720         spam bounces are not sent (the default).
721
722 Invisible
723         If false (the default) fetchmail generates a Received line into
724         each message and generates a HELO from the machine it is running on.
725         If true, fetchmail generates no Received line and HELOs as if it were
726         the remote site.
727
728 In the `Remote Mail Configurations' panel, you can:
729
730 1. Enter the name of a new remote mail server you want fetchmail to query.
731
732 To do this, simply enter a label for the poll configuration in the
733 `New Server:' box.  The label should be a DNS name of the server (unless
734 you are using ssh or some other tunneling method and will fill in the `via'
735 option on the site configuration screen).
736
737 2. Change the configuration of an existing site.
738
739 To do this, find the site's label in the listbox and double-click it.
740 This will take you to a site configuration dialogue.
741 """}
742
743
744 class ConfigurationEdit(Frame, MyWidget):
745     def __init__(self, configuration, outfile, master, onexit):
746         self.subwidgets = {}
747         self.configuration = configuration
748         self.outfile = outfile
749         self.container = master
750         self.onexit = onexit
751         ConfigurationEdit.mode_to_help = {
752             'novice':configure_novice_help, 'expert':configure_expert_help
753             }
754
755     def server_edit(self, sitename):
756         self.subwidgets[sitename] = ServerEdit(sitename, self).edit(self.mode, Toplevel())
757
758     def server_delete(self, sitename):
759         try:
760             for user in self.subwidgets.keys():
761                 user.destruct()
762             del self.configuration[sitename]
763         except:
764             pass
765
766     def edit(self, mode):
767         self.mode = mode
768         Frame.__init__(self, self.container)
769         self.master.title('fetchmail ' + self.mode + ' configurator');
770         self.master.iconname('fetchmail ' + self.mode + ' configurator');
771         self.master.protocol('WM_DELETE_WINDOW', self.nosave)
772         self.keepalive = []     # Use this to anchor the PhotoImage object
773         make_icon_window(self, fetchmail_icon)
774         Pack.config(self)
775         self.post(Configuration, 'configuration')
776
777         dispose_window(self,
778                        'Configurator ' + self.mode + ' Controls',
779                        ConfigurationEdit.mode_to_help[self.mode],
780                        'Save')
781
782         gf = Frame(self, relief=RAISED, bd = 5)
783         Label(gf,
784                 text='Fetchmail Run Controls',
785                 bd=2).pack(side=TOP, pady=10)
786
787         df = Frame(gf)
788
789         ff = Frame(df)
790         if self.mode != 'novice':
791             # Set the postmaster
792             log = LabeledEntry(ff, '     Postmaster:', self.postmaster, '14')
793             log.pack(side=RIGHT, anchor=E)
794
795         # Set the poll interval
796         de = LabeledEntry(ff, '     Poll interval:', self.poll_interval, '14')
797         de.pack(side=RIGHT, anchor=E)
798         ff.pack()
799
800         df.pack()
801
802         if self.mode != 'novice':
803             pf = Frame(gf)
804             Checkbutton(pf,
805                 {'text':'Bounces to sender?',
806                 'variable':self.bouncemail,
807                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
808             pf.pack(fill=X)
809
810             sb = Frame(gf)
811             Checkbutton(sb,
812                 {'text':'send spam bounces?',
813                 'variable':self.spambounce,
814                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
815             sb.pack(fill=X)
816
817             sf = Frame(gf)
818             Checkbutton(sf,
819                 {'text':'Log to syslog?',
820                 'variable':self.syslog,
821                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
822             log = LabeledEntry(sf, '     Logfile:', self.logfile, '14')
823             log.pack(side=RIGHT, anchor=E)
824             sf.pack(fill=X)
825
826             Checkbutton(gf,
827                 {'text':'Invisible mode?',
828                 'variable':self.invisible,
829                  'relief':GROOVE}).pack(side=LEFT, anchor=W)
830             # Set the idfile
831             log = LabeledEntry(gf, '     Idfile:', self.idfile, '14')
832             log.pack(side=RIGHT, anchor=E)
833
834         gf.pack(fill=X)
835
836         # Expert mode allows us to edit multiple sites
837         lf = Frame(self, relief=RAISED, bd=5)
838         Label(lf,
839               text='Remote Mail Server Configurations',
840               bd=2).pack(side=TOP, pady=10)
841         ListEdit('New Server:',
842                 map(lambda x: x.pollname, self.configuration.servers),
843                 lambda site, self=self: self.server_edit(site),
844                 lambda site, self=self: self.server_delete(site),
845                 lf, remotehelp)
846         lf.pack(fill=X)
847
848     def destruct(self):
849         for sitename in self.subwidgets.keys():
850             self.subwidgets[sitename].destruct()
851         self.master.destroy()
852         self.onexit()
853
854     def nosave(self):
855         if ConfirmQuit(self, self.mode + " configuration editor"):
856             self.destruct()
857
858     def save(self):
859         for sitename in self.subwidgets.keys():
860             self.subwidgets[sitename].save()
861         self.fetch(Configuration, 'configuration')
862         fm = None
863         if not self.outfile:
864             fm = sys.stdout
865         elif not os.path.isfile(self.outfile) or Dialog(self,
866                  title = 'Overwrite existing run control file?',
867                  text = 'Really overwrite existing run control file?',
868                  bitmap = 'question',
869                  strings = ('Yes', 'No'),
870                  default = 1).num == 0:
871             try:
872                 os.rename(self.outfile, self.outfile + "~")
873             # Pre-1.5.2 compatibility...
874             except os.error:
875                 pass
876             oldumask = os.umask(077)
877             fm = open(self.outfile, 'w')
878             os.umask(oldumask)
879         if fm:
880             # be paranoid
881             if fm != sys.stdout:
882                 os.chmod(self.outfile, 0600)
883             fm.write("# Configuration created %s by fetchmailconf %s\n" % (time.ctime(time.time()), version))
884             fm.write(`self.configuration`)
885             if self.outfile:
886                 fm.close()
887             self.destruct()
888
889 #
890 # Server editing stuff.
891 #
892 remotehelp = {
893     'title' : 'Remote site help',
894     'banner': 'Remote sites',
895     'text' : """
896 When you add a site name to the list here,
897 you initialize an entry telling fetchmail
898 how to poll a new site.
899
900 When you select a sitename (by double-
901 clicking it, or by single-clicking to
902 select and then clicking the Edit button),
903 you will open a window to configure that
904 site.
905 """}
906
907 serverhelp = {
908     'title' : 'Server options help',
909     'banner': 'Server Options',
910     'text' : """
911 The server options screen controls fetchmail
912 options that apply to one of your mailservers.
913
914 Once you have a mailserver configuration set
915 up as you like it, you can select `OK' to
916 store it in the server list maintained in
917 the main configuration window.
918
919 If you wish to discard changes to a server
920 configuration, select `Quit'.
921 """}
922
923 controlhelp = {
924     'title' : 'Run Control help',
925     'banner': 'Run Controls',
926     'text' : """
927 If the `Poll normally' checkbox is on, the host is polled as part of
928 the normal operation of fetchmail when it is run with no arguments.
929 If it is off, fetchmail will only query this host when it is given as
930 a command-line argument.
931
932 The `True name of server' box should specify the actual DNS name
933 to query. By default this is the same as the poll name.
934
935 Normally each host described in the file is queried once each
936 poll cycle. If `Cycles to skip between polls' is greater than 0,
937 that's the number of poll cycles that are skipped between the
938 times this post is actually polled.
939
940 The `Server timeout' is the number of seconds fetchmail will wait
941 for a reply from the mailserver before concluding it is hung and
942 giving up.
943 """}
944
945 protohelp = {
946     'title' : 'Protocol and Port help',
947     'banner': 'Protocol and Port',
948     'text' : """
949 These options control the remote-mail protocol
950 and TCP/IP service port used to query this
951 server.
952
953 If you click the `Probe for supported protocols'
954 button, fetchmail will try to find you the most
955 capable server on the selected host (this will
956 only work if you're conncted to the Internet).
957 The probe only checks for ordinary IMAP and POP
958 protocols; fortunately these are the most
959 frequently supported.
960
961 The `Protocol' button bar offers you a choice of
962 all the different protocols available.  The `auto'
963 protocol is the default mode; it probes the host
964 ports for POP3 and IMAP to see if either is
965 available.
966
967 Normally the TCP/IP service port to use is
968 dictated by the protocol choice.  The `Service'
969 field (only present in expert mode) lets you
970 set a non-standard service (port).
971 """}
972
973 sechelp = {
974     'title' : 'Security option help',
975     'banner': 'Security',
976     'text' : """
977 The 'authorization mode' allows you to choose the
978 mode that fetchmail uses to log in to your server. You
979 can usually leave this at 'any', but you will have to pick
980 'NTLM' and 'MSN' manually for the nonce.
981
982 The 'interface' option allows you to specify a range
983 of IP addresses to monitor for activity.  If these
984 addresses are not active, fetchmail will not poll.
985 Specifying this may protect you from a spoofing attack
986 if your client machine has more than one IP gateway
987 address and some of the gateways are to insecure nets.
988
989 The `monitor' option, if given, specifies the only
990 device through which fetchmail is permitted to connect
991 to servers.  This option may be used to prevent
992 fetchmail from triggering an expensive dial-out if the
993 interface is not already active.
994
995 The `interface' and `monitor' options are available
996 only for Linux and freeBSD systems.  See the fetchmail
997 manual page for details on these.
998
999 The ssl option enables SSL communication with a mailserver
1000 supporting Secure Sockets Layer. The sslkey and sslcert options
1001 declare key and certificate files for use with SSL.
1002 The sslcertck option enables strict checking of SSL server
1003 certificates (and sslcertpath gives trusted certificate
1004 directory). With sslfingerprint, you can specify a finger-
1005 print the server's key is checked against.
1006 """}
1007
1008 multihelp = {
1009     'title' : 'Multidrop option help',
1010     'banner': 'Multidrop',
1011     'text' : """
1012 These options are only useful with multidrop mode.
1013 See the manual page for extended discussion.
1014 """}
1015
1016 suserhelp = {
1017     'title' : 'User list help',
1018     'banner': 'User list',
1019     'text' : """
1020 When you add a user name to the list here,
1021 you initialize an entry telling fetchmail
1022 to poll the site on behalf of the new user.
1023
1024 When you select a username (by double-
1025 clicking it, or by single-clicking to
1026 select and then clicking the Edit button),
1027 you will open a window to configure the
1028 user's options on that site.
1029 """}
1030
1031 class ServerEdit(Frame, MyWidget):
1032     def __init__(self, host, parent):
1033         self.parent = parent
1034         self.server = None
1035         self.subwidgets = {}
1036         for site in parent.configuration.servers:
1037             if site.pollname == host:
1038                 self.server = site
1039         if (self.server == None):
1040                 self.server = Server()
1041                 self.server.pollname = host
1042                 self.server.via = None
1043                 parent.configuration.servers.append(self.server)
1044
1045     def edit(self, mode, master=None):
1046         Frame.__init__(self, master)
1047         Pack.config(self)
1048         self.master.title('Fetchmail host ' + self.server.pollname);
1049         self.master.iconname('Fetchmail host ' + self.server.pollname);
1050         self.post(Server, 'server')
1051         self.makeWidgets(self.server.pollname, mode)
1052         self.keepalive = []     # Use this to anchor the PhotoImage object
1053         make_icon_window(self, fetchmail_icon)
1054 #       self.grab_set()
1055 #       self.focus_set()
1056 #       self.wait_window()
1057         return self
1058
1059     def destruct(self):
1060         for username in self.subwidgets.keys():
1061             self.subwidgets[username].destruct()
1062         del self.parent.subwidgets[self.server.pollname]
1063         self.master.destroy()
1064
1065     def nosave(self):
1066         if ConfirmQuit(self, 'server option editing'):
1067             self.destruct()
1068
1069     def save(self):
1070         self.fetch(Server, 'server')
1071         for username in self.subwidgets.keys():
1072             self.subwidgets[username].save()
1073         self.destruct()
1074
1075     def defaultPort(self):
1076         proto = self.protocol.get()
1077         # Callback to reset the port number whenever the protocol type changes.
1078         # We used to only reset the port if it had a default (zero) value.
1079         # This turns out to be a bad idea especially in Novice mode -- if
1080         # you set POP3 and then set IMAP, the port invisibly remained 110.
1081         # Now we reset unconditionally on the theory that if you're setting
1082         # a custom port number you should be in expert mode and playing
1083         # close enough attention to notice this...
1084         self.service.set(defaultports[proto])
1085         if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED
1086
1087     def user_edit(self, username, mode):
1088         self.subwidgets[username] = UserEdit(username, self).edit(mode, Toplevel())
1089
1090     def user_delete(self, username):
1091         if self.subwidgets.has_key(username):
1092             self.subwidgets[username].destruct()
1093         del self.server[username]
1094
1095     def makeWidgets(self, host, mode):
1096         topwin = dispose_window(self, "Server options for querying " + host, serverhelp)
1097
1098         leftwin = Frame(self);
1099         leftwidth = '25';
1100
1101         if mode != 'novice':
1102             ctlwin = Frame(leftwin, relief=RAISED, bd=5)
1103             Label(ctlwin, text="Run Controls").pack(side=TOP)
1104             Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP)
1105             LabeledEntry(ctlwin, 'True name of ' + host + ':',
1106                       self.via, leftwidth).pack(side=TOP, fill=X)
1107             LabeledEntry(ctlwin, 'Cycles to skip between polls:',
1108                       self.interval, leftwidth).pack(side=TOP, fill=X)
1109             LabeledEntry(ctlwin, 'Server timeout (seconds):',
1110                       self.timeout, leftwidth).pack(side=TOP, fill=X)
1111             Button(ctlwin, text='Help', fg='blue',
1112                command=lambda: helpwin(controlhelp)).pack(side=RIGHT)
1113             ctlwin.pack(fill=X)
1114
1115         # Compute the available protocols from the compile-time options
1116         protolist = ['auto']
1117         if 'pop2' in feature_options:
1118             protolist.append("POP2")
1119         if 'pop3' in feature_options:
1120             protolist = protolist + ["POP3", "APOP", "KPOP"]
1121         if 'sdps' in feature_options:
1122             protolist.append("SDPS")
1123         if 'imap' in feature_options:
1124             protolist.append("IMAP")
1125         if 'etrn' in feature_options:
1126             protolist.append("ETRN")
1127         if 'odmr' in feature_options:
1128             protolist.append("ODMR")
1129
1130         protwin = Frame(leftwin, relief=RAISED, bd=5)
1131         Label(protwin, text="Protocol").pack(side=TOP)
1132         ButtonBar(protwin, '',
1133                   self.protocol, protolist, 2,
1134                   self.defaultPort)
1135         if mode != 'novice':
1136             LabeledEntry(protwin, 'On server TCP/IP service:',
1137                       self.service, leftwidth).pack(side=TOP, fill=X)
1138             self.defaultPort()
1139             Checkbutton(protwin,
1140                 text="POP3: track `seen' with client-side UIDLs?",
1141                 variable=self.uidl).pack(side=TOP)
1142         Button(protwin, text='Probe for supported protocols', fg='blue',
1143                command=self.autoprobe).pack(side=LEFT)
1144         Button(protwin, text='Help', fg='blue',
1145                command=lambda: helpwin(protohelp)).pack(side=RIGHT)
1146         protwin.pack(fill=X)
1147
1148         userwin = Frame(leftwin, relief=RAISED, bd=5)
1149         Label(userwin, text="User entries for " + host).pack(side=TOP)
1150         ListEdit("New user: ",
1151                  map(lambda x: x.remote, self.server.users),
1152                  lambda u, m=mode, s=self: s.user_edit(u, m),
1153                  lambda u, s=self: s.user_delete(u),
1154                  userwin, suserhelp)
1155         userwin.pack(fill=X)
1156
1157         leftwin.pack(side=LEFT, anchor=N, fill=X);
1158
1159         if mode != 'novice':
1160             rightwin = Frame(self);
1161
1162             mdropwin = Frame(rightwin, relief=RAISED, bd=5)
1163             Label(mdropwin, text="Multidrop options").pack(side=TOP)
1164             LabeledEntry(mdropwin, 'Envelope address header:',
1165                       self.envelope, '22').pack(side=TOP, fill=X)
1166             LabeledEntry(mdropwin, 'Envelope headers to skip:',
1167                       self.envskip, '22').pack(side=TOP, fill=X)
1168             LabeledEntry(mdropwin, 'Name prefix to strip:',
1169                       self.qvirtual, '22').pack(side=TOP, fill=X)
1170             Checkbutton(mdropwin, text="Enable multidrop DNS lookup?",
1171                     variable=self.dns).pack(side=TOP)
1172             Label(mdropwin, text="DNS aliases").pack(side=TOP)
1173             ListEdit("New alias: ", self.server.aka, None, None, mdropwin, None)
1174             Label(mdropwin, text="Domains to be considered local").pack(side=TOP)
1175             ListEdit("New domain: ",
1176                  self.server.localdomains, None, None, mdropwin, multihelp)
1177             mdropwin.pack(fill=X)
1178
1179             if os_type in ('linux', 'freebsd'):
1180                 secwin = Frame(rightwin, relief=RAISED, bd=5)
1181                 Label(secwin, text="Security").pack(side=TOP)
1182                 # Don't actually let users set this.  KPOP sets it implicitly
1183                 ButtonBar(secwin, 'Authorization mode:',
1184                          self.auth, authlist, 2, None).pack(side=TOP)
1185                 if os_type == 'linux' or os_type == 'freebsd'  or 'interface' in dictmembers:
1186                     LabeledEntry(secwin, 'IP range to check before poll:',
1187                          self.interface, leftwidth).pack(side=TOP, fill=X)
1188                 if os_type == 'linux' or os_type == 'freebsd' or 'monitor' in dictmembers:
1189                     LabeledEntry(secwin, 'Interface to monitor:',
1190                          self.monitor, leftwidth).pack(side=TOP, fill=X)
1191                 # Someday this should handle Kerberos 5 too
1192                 if 'kerberos' in feature_options:
1193                     LabeledEntry(secwin, 'Principal:',
1194                          self.principal, '12').pack(side=TOP, fill=X)
1195                 # ESMTP authentication
1196                 LabeledEntry(secwin, 'ESMTP name:',
1197                              self.esmtpname, '12').pack(side=TOP, fill=X)
1198                 LabeledEntry(secwin, 'ESMTP password:',
1199                              self.esmtppassword, '12').pack(side=TOP, fill=X)
1200                 Button(secwin, text='Help', fg='blue',
1201                        command=lambda: helpwin(sechelp)).pack(side=RIGHT)
1202                 secwin.pack(fill=X)
1203
1204             rightwin.pack(side=LEFT, anchor=N);
1205
1206     def autoprobe(self):
1207         # Note: this only handles case (1) near fetchmail.c:1032
1208         # We're assuming people smart enough to set up ssh tunneling
1209         # won't need autoprobing.
1210         if self.server.via:
1211             realhost = self.server.via
1212         else:
1213             realhost = self.server.pollname
1214         greetline = None
1215         for protocol in ("IMAP","POP3","POP2"):
1216             service = defaultports[protocol]
1217             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1218             try:
1219                 sock.connect((realhost, ianaservices[service]))
1220                 greetline = sock.recv(1024)
1221                 sock.close()
1222             except:
1223                 pass
1224             else:
1225                 break
1226         confwin = Toplevel()
1227         if greetline == None:
1228             title = "Autoprobe of " + realhost + " failed"
1229             confirm = """
1230 Fetchmailconf didn't find any mailservers active.
1231 This could mean the host doesn't support any,
1232 or that your Internet connection is down, or
1233 that the host is so slow that the probe timed
1234 out before getting a response.
1235 """
1236         else:
1237             warnings = ''
1238             # OK, now try to recognize potential problems
1239
1240             if protocol == "POP2":
1241                 warnings = warnings + """
1242 It appears you have somehow found a mailserver running only POP2.
1243 Congratulations.  Have you considered a career in archaeology?
1244
1245 Unfortunately, stock fetchmail binaries don't include POP2 support anymore.
1246 Unless the first line of your fetchmail -V output includes the string "POP2",
1247 you'll have to build it from sources yourself with the configure
1248 switch --enable-POP2.
1249
1250 """
1251
1252 ### POP3 servers start here
1253
1254             if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.004") > 0:
1255                 warnings = warnings + """
1256 This appears to be an old version of the UC Davis POP server.  These are
1257 dangerously unreliable (among other problems, they may drop your mailbox
1258 on the floor if your connection is interrupted during the session).
1259
1260 It is strongly recommended that you find a better POP3 server.  The fetchmail
1261 FAQ includes pointers to good ones.
1262
1263 """
1264             if string.find(greetline, "comcast.net") > 0:
1265                 warnings = warnings + """
1266 The Comcast Maillennium POP3 server only returns the first 80K of a long
1267 message retrieved with TOP. Its response to RETR is normal, so use the
1268 `fetchall' option.
1269
1270 """
1271 # Steve VanDevender <stevev@efn.org> writes:
1272 # The only system I have seen this happen with is cucipop-1.31
1273 # under SunOS 4.1.4.  cucipop-1.31 runs fine on at least Solaris
1274 # 2.x and probably quite a few other systems.  It appears to be a
1275 # bug or bad interaction with the SunOS realloc() -- it turns out
1276 # that internally cucipop does allocate a certain data structure in
1277 # multiples of 16, using realloc() to bump it up to the next
1278 # multiple if it needs more.
1279 #
1280 # The distinctive symptom is that when there are 16 messages in the
1281 # inbox, you can RETR and DELE all 16 messages successfully, but on
1282 # QUIT cucipop returns something like "-ERR Error locking your
1283 # mailbox" and aborts without updating it.
1284 #
1285 # The cucipop banner looks like:
1286 #
1287 # +OK Cubic Circle's v1.31 1998/05/13 POP3 ready <6229000062f95036@wakko>
1288 #
1289             if string.find(greetline, "Cubic Circle") > 0:
1290                 warnings = warnings + """
1291 I see your server is running cucipop.  Better make sure the server box
1292 isn't a SunOS 4.1.4 machine; cucipop tickles a bug in SunOS realloc()
1293 under that version, and doesn't cope with the result gracefully.  Newer
1294 SunOS and Solaris machines run cucipop OK.
1295
1296 Also, some versions of cucipop don't assert an exclusive lock on your
1297 mailbox when it's being queried.  This means that if you have more than
1298 one fetchmail query running against the same mailbox, bad things can happen.
1299 """
1300             if string.find(greetline, "David POP3 Server") > 0:
1301                 warnings = warnings + """
1302 This POP3 server is badly broken.  You should get rid of it -- and the
1303 brain-dead Microsoft operating system it rode in on.
1304
1305 """
1306 # The greeting line on the server known to be buggy is:
1307 # +OK POP3 server ready (running FTGate V2, 2, 1, 0 Jun 21 1999 09:55:01)
1308 #
1309             if string.find(greetline, "FTGate") > 0:
1310                 warnings = warnings + """
1311 This POP server has a weird bug; it says OK twice in response to TOP.
1312 Its response to RETR is normal, so use the `fetchall' option.
1313
1314 """
1315             if string.find(greetline, " geonet.de") > 0:
1316                 warnings = warnings + """
1317 You appear to be using geonet.  As of late 2002, the TOP command on
1318 geonet's POP3 is broken.  Use the fetchall option.
1319
1320 """
1321             if string.find(greetline, "OpenMail") > 0:
1322                 warnings = warnings + """
1323 You appear to be using some version of HP OpenMail.  Many versions of
1324 OpenMail do not process the "TOP" command correctly; the symptom is that
1325 only the header and first line of each message is retrieved.  To work
1326 around this bug, turn on `fetchall' on all user entries associated with
1327 this server.
1328
1329 """
1330             if string.find(greetline, "Escape character is") > 0:
1331                 warnings = warnings + """
1332 Your greeting line looks like it was written by a fetid pile of
1333 camel dung identified to me as `popa3d written by Solar Designer'.
1334 Beware!  The UIDL support in this thing is known to be completely broken,
1335 and other things probably are too.
1336
1337 """
1338             if string.find(greetline, "MercuryP/NLM v1.48") > 0:
1339                 warnings = warnings + """
1340 This is not a POP3 server.  It has delusions of being one, but after
1341 RETR all messages are automatically marked to be deleted.  The only
1342 way to prevent this is to issue an RSET before leaving the server.
1343 Fetchmail does this, but we suspect this is probably broken in lots
1344 of other ways, too.
1345
1346 """
1347             if string.find(greetline, "POP-Max") > 0:
1348                 warnings = warnings + """
1349 The Mail Max POP3 server screws up on mail with attachments.  It
1350 reports the message size with attachments included, but doesn't
1351 download them on a RETR or TOP (this violates the IMAP RFCs).  It also
1352 doesn't implement TOP correctly.  You should get rid of it -- and the
1353 brain-dead NT server it rode in on.
1354
1355 """
1356             if string.find(greetline, "POP3 Server Ready") > 0:
1357                 warnings = warnings + """
1358 Some server that uses this greeting line has been observed to choke on
1359 TOP %d 99999999.  Use the fetchall option. if necessary, to force RETR.
1360
1361 """
1362             if string.find(greetline, "QPOP") > 0:
1363                 warnings = warnings + """
1364 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1365 knows all about qpopper.  However, be aware that the 2.53 version of
1366 qpopper does something odd that causes fetchmail to hang with a socket
1367 error on very large messages.  This is probably not a fetchmail bug, as
1368 it has been observed with fetchpop.  The fix is to upgrade to qpopper
1369 3.0beta or a more recent version.  Better yet, switch to IMAP.
1370
1371 """
1372             if string.find(greetline, " sprynet.com") > 0:
1373                 warnings = warnings + """
1374 You appear to be using a SpryNet server.  In mid-1999 it was reported that
1375 the SpryNet TOP command marks messages seen.  Therefore, for proper error
1376 recovery in the event of a line drop, it is strongly recommended that you
1377 turn on `fetchall' on all user entries associated with this server.
1378
1379 """
1380             if string.find(greetline, "TEMS POP3") > 0:
1381                 warnings = warnings + """
1382 Your POP3 server has "TEMS" in its header line.  At least one such
1383 server does not process the "TOP" command correctly; the symptom is
1384 that fetchmail hangs when trying to retrieve mail.  To work around
1385 this bug, turn on `fetchall' on all user entries associated with this
1386 server.
1387
1388 """
1389             if string.find(greetline, " spray.se") > 0:
1390                 warnings = warnings + """
1391 Your POP3 server has "spray.se" in its header line.  In May 2000 at
1392 least one such server did not process the "TOP" command correctly; the
1393 symptom is that messages are treated as headerless.  To work around
1394 this bug, turn on `fetchall' on all user entries associated with this
1395 server.
1396
1397 """
1398             if string.find(greetline, " usa.net") > 0:
1399                 warnings = warnings + """
1400 You appear to be using USA.NET's free mail service.  Their POP3 servers
1401 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
1402 fetchmail can compensate.  They seem to require that fetchall be switched on
1403 (otherwise you won't necessarily see all your mail, not even new mail).
1404 They also botch the TOP command the fetchmail normally uses for retrieval
1405 (it only retrieves about 10 lines rather than the number specified).
1406 Turning on fetchall will disable the use of TOP.
1407
1408 Therefore, it is strongly recommended that you turn on `fetchall' on all
1409 user entries associated with this server.
1410
1411 """
1412             if string.find(greetline, " Novonyx POP3") > 0:
1413                 warnings = warnings + """
1414 Your mailserver is running Novonyx POP3.  This server, at least as of
1415 version 2.17, seems to have problems handling and reporting seen bits.
1416 You may have to use the fetchall option.
1417
1418 """
1419             if string.find(greetline, " IMS POP3") > 0:
1420                 warnings = warnings + """
1421 Some servers issuing the greeting line 'IMS POP3' have been known to
1422 do byte-stuffing incorrectly.  This means that if a message you receive
1423 has a . (period) at start of line, fetchmail will become confused and
1424 probably wedge itself.  (This bug was recorded on IMS POP3 0.86.)
1425
1426 """
1427
1428 ### IMAP servers start here
1429
1430             if string.find(greetline, "GroupWise") > 0:
1431                 warnings = warnings + """
1432 The Novell GroupWise IMAP server would be better named GroupFoolish;
1433 it is (according to the designer of IMAP) unusably broken.  Among
1434 other things, it doesn't include a required content length in its
1435 BODY[TEXT] response.<p>
1436
1437 Fetchmail works around this problem, but we strongly recommend voting
1438 with your dollars for a server that isn't brain-dead.  If you stick
1439 with code as shoddy as GroupWise seems to be, you will probably pay
1440 for it with other problems.<p>
1441
1442 """
1443             if string.find(greetline, "InterChange") > 0:
1444                 warnings = warnings + """
1445
1446 The InterChange IMAP server at release levels below 3.61.08 screws up
1447 on mail with attachments.  It doesn't fetch them if you give it a
1448 BODY[TEXT] request, though it does if you request RFC822.TEXT.
1449 According to the IMAP RFCs and their maintainer these should be
1450 equivalent -- and we can't drop the BODY[TEXT] form because M$
1451 Exchange (quite legally under RFC2062) rejectsit.  The InterChange
1452 folks claim to have fixed this bug in 3.61.08.
1453
1454 """
1455             if string.find(greetline, "Imail") > 0:
1456                 warnings = warnings + """
1457 We've seen a bug report indicating that this IMAP server (at least as of
1458 version 5.0.7) returns an invalid body size for messages with MIME
1459 attachments; the effect is to drop the attachments on the floor.  We
1460 recommend you upgrade to a non-broken IMAP server.
1461
1462 """
1463             if string.find(greetline, "Domino IMAP4") > 0:
1464                 warnings = warnings + """
1465 Your IMAP server appears to be Lotus Domino.  This server, at least up
1466 to version 4.6.2a, has a bug in its generation of MIME boundaries (see
1467 the details in the fetchmail FAQ).  As a result, even MIME aware MUAs
1468 will see attachments as part of the message text.  If your Domino server's
1469 POP3 facility is enabled, we recommend you fall back on it.
1470
1471 """
1472
1473 ### Checks for protocol variants start here
1474
1475             closebrak = string.find(greetline, ">")
1476             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1477                 warnings = warnings + """
1478 It looks like you could use APOP on this server and avoid sending it your
1479 password in clear.  You should talk to the mailserver administrator about
1480 this.
1481
1482 """
1483             if string.find(greetline, "IMAP2bis") > 0:
1484                 warnings = warnings + """
1485 IMAP2bis servers have a minor problem; they can't peek at messages without
1486 marking them seen.  If you take a line hit during the retrieval, the
1487 interrupted message may get left on the server, marked seen.
1488
1489 To work around this, it is recommended that you set the `fetchall'
1490 option on all user entries associated with this server, so any stuck
1491 mail will be retrieved next time around.
1492
1493 To fix this bug, upgrade to an IMAP4 server.  The fetchmail FAQ includes
1494 a pointer to an open-source implementation.
1495
1496 """
1497             if string.find(greetline, "IMAP4rev1") > 0:
1498                 warnings = warnings + """
1499 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1500 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1501 has therefore been extremely well tested with this class of server.
1502
1503 """
1504             if warnings == '':
1505                 warnings = warnings + """
1506 Fetchmail doesn't know anything special about this server type.
1507
1508 """
1509
1510             # Display success window with warnings
1511             title = "Autoprobe of " + realhost + " succeeded"
1512             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1513             self.protocol.set(protocol)
1514             self.service.set(defaultports[protocol])
1515         confwin.title(title)
1516         confwin.iconname(title)
1517         Label(confwin, text=title).pack()
1518         Message(confwin, text=confirm, width=600).pack()
1519         Button(confwin, text='Done',
1520                    command=lambda x=confwin: x.destroy(), bd=2).pack()
1521
1522 #
1523 # User editing stuff
1524 #
1525
1526 userhelp = {
1527     'title' : 'User option help',
1528     'banner': 'User options',
1529     'text' : """
1530 You may use this panel to set options
1531 that may differ between individual
1532 users on your site.
1533
1534 Once you have a user configuration set
1535 up as you like it, you can select `OK' to
1536 store it in the user list maintained in
1537 the site configuration window.
1538
1539 If you wish to discard the changes you have
1540 made to user options, select `Quit'.
1541 """}
1542
1543 localhelp = {
1544     'title' : 'Local name help',
1545     'banner': 'Local names',
1546     'text' : """
1547 The local name(s) in a user entry are the
1548 people on the client machine who should
1549 receive mail from the poll described.
1550
1551 Note: if a user entry has more than one
1552 local name, messages will be retrieved
1553 in multidrop mode.  This complicates
1554 the configuration issues; see the manual
1555 page section on multidrop mode.
1556
1557 Warning: Be careful with local names
1558 such as foo@bar.com, as that can cause
1559 the mail to be sent to foo@bar.com instead
1560 of sending it to your local system.
1561 """}
1562
1563 class UserEdit(Frame, MyWidget):
1564     def __init__(self, username, parent):
1565         self.parent = parent
1566         self.user = None
1567         for user in parent.server.users:
1568             if user.remote == username:
1569                 self.user = user
1570         if self.user == None:
1571             self.user = User()
1572             self.user.remote = username
1573             self.user.localnames = [username]
1574             parent.server.users.append(self.user)
1575
1576     def edit(self, mode, master=None):
1577         Frame.__init__(self, master)
1578         Pack.config(self)
1579         self.master.title('Fetchmail user ' + self.user.remote
1580                           + ' querying ' + self.parent.server.pollname);
1581         self.master.iconname('Fetchmail user ' + self.user.remote);
1582         self.post(User, 'user')
1583         self.makeWidgets(mode, self.parent.server.pollname)
1584         self.keepalive = []     # Use this to anchor the PhotoImage object
1585         make_icon_window(self, fetchmail_icon)
1586 #       self.grab_set()
1587 #       self.focus_set()
1588 #       self.wait_window()
1589         return self
1590
1591     def destruct(self):
1592         # Yes, this test can fail -- if you delete the parent window.
1593         if self.parent.subwidgets.has_key(self.user.remote):
1594             del self.parent.subwidgets[self.user.remote]
1595         self.master.destroy()
1596
1597     def nosave(self):
1598         if ConfirmQuit(self, 'user option editing'):
1599             self.destruct()
1600
1601     def save(self):
1602         ok = 0
1603         for x in self.user.localnames: ok = ok + (string.find(x, '@') != -1)
1604         if ok == 0 or  Dialog(self,
1605             title = "Really accept an embedded '@' ?",
1606             text = "Local names with an embedded '@', such as in foo@bar "
1607                    "might result in your mail being sent to foo@bar.com "
1608                    "instead of your local system.\n Are you sure you want "
1609                    "a local user name with an '@' in it?",
1610             bitmap = 'question',
1611             strings = ('Yes', 'No'),
1612             default = 1).num == 0:
1613                 self.fetch(User, 'user')
1614                 self.destruct()
1615
1616     def makeWidgets(self, mode, servername):
1617         dispose_window(self,
1618                         "User options for " + self.user.remote + " querying " + servername,
1619                         userhelp)
1620
1621         if mode != 'novice':
1622             leftwin = Frame(self);
1623         else:
1624             leftwin = self
1625
1626         secwin = Frame(leftwin, relief=RAISED, bd=5)
1627         Label(secwin, text="Authentication").pack(side=TOP)
1628         LabeledEntry(secwin, 'Password:',
1629                       self.password, '12').pack(side=TOP, fill=X)
1630         secwin.pack(fill=X, anchor=N)
1631
1632         if 'ssl' in feature_options or 'ssl' in dictmembers:
1633             sslwin = Frame(leftwin, relief=RAISED, bd=5)
1634             Checkbutton(sslwin, text="Use SSL?",
1635                         variable=self.ssl).pack(side=TOP, fill=X)
1636             LabeledEntry(sslwin, 'SSL key:',
1637                          self.sslkey, '14').pack(side=TOP, fill=X)
1638             LabeledEntry(sslwin, 'SSL certificate:',
1639                          self.sslcert, '14').pack(side=TOP, fill=X)
1640             Checkbutton(sslwin, text="Check server SSL certificate?",
1641                         variable=self.sslcertck).pack(side=TOP, fill=X)
1642             LabeledEntry(sslwin, 'SSL trusted certificate directory:',
1643                          self.sslcertpath, '14').pack(side=TOP, fill=X)
1644             LabeledEntry(sslwin, 'SSL key fingerprint:',
1645                          self.sslfingerprint, '14').pack(side=TOP, fill=X)
1646             sslwin.pack(fill=X, anchor=N)
1647
1648         names = Frame(leftwin, relief=RAISED, bd=5)
1649         Label(names, text="Local names").pack(side=TOP)
1650         ListEdit("New name: ",
1651                      self.user.localnames, None, None, names, localhelp)
1652         names.pack(fill=X, anchor=N)
1653
1654         if mode != 'novice':
1655             targwin = Frame(leftwin, relief=RAISED, bd=5)
1656             Label(targwin, text="Forwarding Options").pack(side=TOP)
1657             Label(targwin, text="Listeners to forward to").pack(side=TOP)
1658             ListEdit("New listener:",
1659                      self.user.smtphunt, None, None, targwin, None)
1660             Label(targwin, text="Domains to fetch from (ODMR/ETRN only)").pack(side=TOP)
1661             ListEdit("Domains:",
1662                      self.user.fetchdomains, None, None, targwin, None)
1663             LabeledEntry(targwin, 'Append to MAIL FROM line:',
1664                      self.smtpaddress, '26').pack(side=TOP, fill=X)
1665             LabeledEntry(targwin, 'Set RCPT To address:',
1666                      self.smtpname, '26').pack(side=TOP, fill=X)
1667             LabeledEntry(targwin, 'Connection setup command:',
1668                      self.preconnect, '26').pack(side=TOP, fill=X)
1669             LabeledEntry(targwin, 'Connection wrapup command:',
1670                      self.postconnect, '26').pack(side=TOP, fill=X)
1671             LabeledEntry(targwin, 'Local delivery agent:',
1672                      self.mda, '26').pack(side=TOP, fill=X)
1673             LabeledEntry(targwin, 'BSMTP output file:',
1674                      self.bsmtp, '26').pack(side=TOP, fill=X)
1675             LabeledEntry(targwin, 'Listener spam-block codes:',
1676                      self.antispam, '26').pack(side=TOP, fill=X)
1677             LabeledEntry(targwin, 'Pass-through properties:',
1678                      self.properties, '26').pack(side=TOP, fill=X)
1679             Checkbutton(targwin, text="Use LMTP?",
1680                         variable=self.lmtp).pack(side=TOP, fill=X)
1681             targwin.pack(fill=X, anchor=N)
1682
1683         if mode != 'novice':
1684             leftwin.pack(side=LEFT, fill=X, anchor=N)
1685             rightwin = Frame(self)
1686         else:
1687             rightwin = self
1688
1689         optwin = Frame(rightwin, relief=RAISED, bd=5)
1690         Label(optwin, text="Processing Options").pack(side=TOP)
1691         Checkbutton(optwin, text="Suppress deletion of messages after reading",
1692                     variable=self.keep).pack(side=TOP, anchor=W)
1693         Checkbutton(optwin, text="Fetch old messages as well as new",
1694                     variable=self.fetchall).pack(side=TOP, anchor=W)
1695         if mode != 'novice':
1696             Checkbutton(optwin, text="Flush seen messages before retrieval",
1697                     variable=self.flush).pack(side=TOP, anchor=W)
1698             Checkbutton(optwin, text="Flush oversized messages before retrieval",
1699                     variable=self.limitflush).pack(side=TOP, anchor=W)
1700             Checkbutton(optwin, text="Rewrite To/Cc/Bcc messages to enable reply",
1701                     variable=self.rewrite).pack(side=TOP, anchor=W)
1702             Checkbutton(optwin, text="Force CR/LF at end of each line",
1703                     variable=self.forcecr).pack(side=TOP, anchor=W)
1704             Checkbutton(optwin, text="Strip CR from end of each line",
1705                     variable=self.stripcr).pack(side=TOP, anchor=W)
1706             Checkbutton(optwin, text="Pass 8 bits even though SMTP says 7BIT",
1707                     variable=self.pass8bits).pack(side=TOP, anchor=W)
1708             Checkbutton(optwin, text="Undo MIME armoring on header and body",
1709                     variable=self.mimedecode).pack(side=TOP, anchor=W)
1710             Checkbutton(optwin, text="Drop Status lines from forwarded messages",
1711                     variable=self.dropstatus).pack(side=TOP, anchor=W)
1712             Checkbutton(optwin, text="Drop Delivered-To lines from forwarded messages",
1713                     variable=self.dropdelivered).pack(side=TOP, anchor=W)
1714         optwin.pack(fill=X)
1715
1716         if mode != 'novice':
1717             limwin = Frame(rightwin, relief=RAISED, bd=5)
1718             Label(limwin, text="Resource Limits").pack(side=TOP)
1719             LabeledEntry(limwin, 'Message size limit:',
1720                       self.limit, '30').pack(side=TOP, fill=X)
1721             LabeledEntry(limwin, 'Size warning interval:',
1722                       self.warnings, '30').pack(side=TOP, fill=X)
1723             LabeledEntry(limwin, 'Max messages to fetch per poll:',
1724                       self.fetchlimit, '30').pack(side=TOP, fill=X)
1725             LabeledEntry(limwin, 'Max message sizes to fetch per transaction:',
1726                       self.fetchsizelimit, '30').pack(side=TOP, fill=X)
1727             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1728                 LabeledEntry(limwin, 'Use fast UIDL:',
1729                         self.fastuidl, '30').pack(side=TOP, fill=X)
1730             LabeledEntry(limwin, 'Max messages to forward per poll:',
1731                       self.batchlimit, '30').pack(side=TOP, fill=X)
1732             if self.parent.server.protocol not in ('ETRN', 'ODMR'):
1733                 LabeledEntry(limwin, 'Interval between expunges:',
1734                              self.expunge, '30').pack(side=TOP, fill=X)
1735             Checkbutton(limwin, text="Idle after each poll (IMAP only)",
1736                     variable=self.idle).pack(side=TOP, anchor=W)
1737             limwin.pack(fill=X)
1738
1739             if self.parent.server.protocol == 'IMAP':
1740                 foldwin = Frame(rightwin, relief=RAISED, bd=5)
1741                 Label(foldwin, text="Remote folders (IMAP only)").pack(side=TOP)
1742                 ListEdit("New folder:", self.user.mailboxes,
1743                          None, None, foldwin, None)
1744                 foldwin.pack(fill=X, anchor=N)
1745
1746         if mode != 'novice':
1747             rightwin.pack(side=LEFT)
1748         else:
1749             self.pack()
1750
1751
1752 #
1753 # Top-level window that offers either novice or expert mode
1754 # (but not both at once; it disappears when one is selected).
1755 #
1756
1757 class Configurator(Frame):
1758     def __init__(self, outfile, master, onexit, parent):
1759         Frame.__init__(self, master)
1760         self.outfile = outfile
1761         self.onexit = onexit
1762         self.parent = parent
1763         self.master.title('fetchmail configurator');
1764         self.master.iconname('fetchmail configurator');
1765         Pack.config(self)
1766         self.keepalive = []     # Use this to anchor the PhotoImage object
1767         make_icon_window(self, fetchmail_icon)
1768
1769         Message(self, text="""
1770 Use `Novice Configuration' for basic fetchmail setup;
1771 with this, you can easily set up a single-drop connection
1772 to one remote mail server.
1773 """, width=600).pack(side=TOP)
1774         Button(self, text='Novice Configuration',
1775                                 fg='blue', command=self.novice).pack()
1776
1777         Message(self, text="""
1778 Use `Expert Configuration' for advanced fetchmail setup,
1779 including multiple-site or multidrop connections.
1780 """, width=600).pack(side=TOP)
1781         Button(self, text='Expert Configuration',
1782                                 fg='blue', command=self.expert).pack()
1783
1784         Message(self, text="""
1785 Or you can just select `Quit' to leave the configurator now and
1786 return to the main panel.
1787 """, width=600).pack(side=TOP)
1788         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1789         master.protocol("WM_DELETE_WINDOW", self.leave)
1790
1791     def novice(self):
1792         self.master.destroy()
1793         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('novice')
1794
1795     def expert(self):
1796         self.master.destroy()
1797         ConfigurationEdit(Fetchmailrc, self.outfile, Toplevel(), self.onexit).edit('expert')
1798
1799     def leave(self):
1800         self.master.destroy()
1801         self.onexit()
1802
1803 # Run a command in a scrolling text widget, displaying its output
1804
1805 class RunWindow(Frame):
1806     def __init__(self, command, master, parent):
1807         Frame.__init__(self, master)
1808         self.master = master
1809         self.master.title('fetchmail run window');
1810         self.master.iconname('fetchmail run window');
1811         Pack.config(self)
1812         Label(self,
1813                 text="Running "+command,
1814                 bd=2).pack(side=TOP, pady=10)
1815         self.keepalive = []     # Use this to anchor the PhotoImage object
1816         make_icon_window(self, fetchmail_icon)
1817
1818         # This is a scrolling text window
1819         textframe = Frame(self)
1820         scroll = Scrollbar(textframe)
1821         self.textwidget = Text(textframe, setgrid=TRUE)
1822         textframe.pack(side=TOP, expand=YES, fill=BOTH)
1823         self.textwidget.config(yscrollcommand=scroll.set)
1824         self.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
1825         scroll.config(command=self.textwidget.yview)
1826         scroll.pack(side=RIGHT, fill=BOTH)
1827         textframe.pack(side=TOP)
1828
1829         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1830
1831         self.update()   # Draw widget before executing fetchmail
1832
1833         # Always look for a runnable command in the directory we're running in
1834         # first. This avoids some obscure version-skew errors that can occur
1835         # if you pick up an old fetchmail from the standard system locations.
1836         os.environ["PATH"] = os.path.dirname(sys.argv[0]) + ":" + os.environ["PATH"]
1837         child_stdout = os.popen(command + " 2>&1 </dev/null", "r")
1838         while 1:
1839             ch = child_stdout.read(1)
1840             if not ch:
1841                 break
1842             self.textwidget.insert(END, ch)
1843         self.textwidget.insert(END, "Done.")
1844         self.textwidget.see(END);
1845
1846     def leave(self):
1847         self.master.destroy()
1848
1849 # Here's where we choose either configuration or launching
1850
1851 class MainWindow(Frame):
1852     def __init__(self, outfile, master=None):
1853         Frame.__init__(self, master)
1854         self.outfile = outfile
1855         self.master.title('fetchmail launcher');
1856         self.master.iconname('fetchmail launcher');
1857         Pack.config(self)
1858         Label(self,
1859                 text='Fetchmailconf ' + version,
1860                 bd=2).pack(side=TOP, pady=10)
1861         self.keepalive = []     # Use this to anchor the PhotoImage object
1862         make_icon_window(self, fetchmail_icon)
1863         self.debug = 0
1864
1865         ## Test icon display with the following:
1866         # icon_image = PhotoImage(data=fetchmail_icon)
1867         # Label(self, image=icon_image).pack(side=TOP, pady=10)
1868         # self.keepalive.append(icon_image)
1869
1870         Message(self, text="""
1871 Use `Configure fetchmail' to tell fetchmail about the remote
1872 servers it should poll (the host name, your username there,
1873 whether to use POP or IMAP, and so forth).
1874 """, width=600).pack(side=TOP)
1875         self.configbutton = Button(self, text='Configure fetchmail',
1876                                 fg='blue', command=self.configure)
1877         self.configbutton.pack()
1878
1879         Message(self, text="""
1880 Use `Run fetchmail' to run fetchmail with debugging enabled.
1881 This is a good way to test out a new configuration.
1882 """, width=600).pack(side=TOP)
1883         Button(self, text='Run fetchmail',fg='blue', command=self.test).pack()
1884
1885         Message(self, text="""
1886 Use `Run fetchmail' to run fetchmail in foreground.
1887 Progress  messages will be shown, but not debug messages.
1888 """, width=600).pack(side=TOP)
1889         Button(self, text='Run fetchmail', fg='blue', command=self.run).pack()
1890
1891         Message(self, text="""
1892 Or you can just select `Quit' to exit the launcher now.
1893 """, width=600).pack(side=TOP)
1894         Button(self, text='Quit', fg='blue', command=self.leave).pack()
1895
1896     def configure(self):
1897         self.configbutton.configure(state=DISABLED)
1898         Configurator(self.outfile, Toplevel(),
1899                      lambda self=self: self.configbutton.configure(state=NORMAL),
1900                      self)
1901     def test(self):
1902         cmd = "fetchmail -N -d0 --nosyslog -v"
1903         if rcfile:
1904             cmd = cmd + " -f " + rcfile
1905         RunWindow(cmd, Toplevel(), self)
1906
1907     def run(self):
1908         cmd = "fetchmail -N -d0"
1909         if rcfile:
1910             cmd = cmd + " -f " + rcfile
1911         RunWindow(cmd, Toplevel(), self)
1912
1913     def leave(self):
1914         self.quit()
1915
1916 # Functions for turning a dictionary into an instantiated object tree.
1917
1918 def intersect(list1, list2):
1919 # Compute set intersection of lists
1920     res = []
1921     for x in list1:
1922         if x in list2:
1923             res.append(x)
1924     return res
1925
1926 def setdiff(list1, list2):
1927 # Compute set difference of lists
1928     res = []
1929     for x in list1:
1930         if not x in list2:
1931             res.append(x)
1932     return res
1933
1934 def copy_instance(toclass, fromdict):
1935 # Initialize a class object of given type from a conformant dictionary.
1936     for fld in fromdict.keys():
1937         if not fld in dictmembers:
1938             dictmembers.append(fld)
1939 # The `optional' fields are the ones we can ignore for purposes of
1940 # conformability checking; they'll still get copied if they are
1941 # present in the dictionary.
1942     optional = ('interface', 'monitor',
1943                 'esmtpname', 'esmtppassword',
1944                 'ssl', 'sslkey', 'sslcert', 'sslproto', 'sslcertck',
1945                 'sslcertpath', 'sslfingerprint', 'showdots')
1946     class_sig = setdiff(toclass.__dict__.keys(), optional)
1947     class_sig.sort()
1948     dict_keys = setdiff(fromdict.keys(), optional)
1949     dict_keys.sort()
1950     common = intersect(class_sig, dict_keys)
1951     if 'typemap' in class_sig:
1952         class_sig.remove('typemap')
1953     if tuple(class_sig) != tuple(dict_keys):
1954         print "Fields don't match what fetchmailconf expected:"
1955 #       print "Class signature: " + `class_sig`
1956 #       print "Dictionary keys: " + `dict_keys`
1957         diff = setdiff(class_sig, common)
1958         if diff:
1959             print "Not matched in class `" + toclass.__class__.__name__ + "' signature: " + `diff`
1960         diff = setdiff(dict_keys, common)
1961         if diff:
1962             print "Not matched in dictionary keys: " + `diff`
1963         sys.exit(1)
1964     else:
1965         for x in fromdict.keys():
1966             setattr(toclass, x, fromdict[x])
1967
1968 #
1969 # And this is the main sequence.  How it works:
1970 #
1971 # First, call `fetchmail --configdump' and trap the output in a tempfile.
1972 # This should fill it with a Python initializer for a variable `fetchmailrc'.
1973 # Run execfile on the file to pull fetchmailrc into Python global space.
1974 # You don't want static data, though; you want, instead, a tree of objects
1975 # with the same data members and added appropriate methods.
1976 #
1977 # This is what the copy_instance function() is for.  It tries to copy a
1978 # dictionary field by field into a class, aborting if the class and dictionary
1979 # have different data members (except for any typemap member in the class;
1980 # that one is strictly for use by the MyWidget supperclass).
1981 #
1982 # Once the object tree is set up, require user to choose novice or expert
1983 # mode and instantiate an edit object for the configuration.  Class methods
1984 # will take it all from there.
1985 #
1986 # Options (not documented because they're for fetchmailconf debuggers only):
1987 # -d: Read the configuration and dump it to stdout before editing.  Dump
1988 #     the edited result to stdout as well.
1989 # -f: specify the run control file to read.
1990
1991 if __name__ == '__main__':
1992
1993     if not os.environ.has_key("DISPLAY"):
1994         print "fetchmailconf must be run under X"
1995         sys.exit(1)
1996
1997     fetchmail_icon = """
1998 R0lGODdhPAAoAPcAAP///wgICBAQEISEhIyMjJSUlKWlpa2trbW1tcbGxs7Ozufn5+/v7//39yEY
1999 GNa9tUoxKZyEe1o5KTEQAN7OxpyMhIRjUvfn3pxSKYQ5EO/Wxv/WvWtSQrVzSmtCKWspAMatnP/e
2000 xu+1jIxSKaV7Wt6ca5xSGK2EY8aUa72MY86UY617UsaMWrV7SpRjOaVrOZRaKYxSIXNCGGs5EIRC
2001 CJR7Y/+UMdbOxnNrY97Ove/Wvd7GrZyEa961jL2Ua9alc86ca7WEUntSKcaMSqVjGNZ7GGM5CNa1
2002 jPfOnN6tc3taMffeve/WtWtaQv/OjGtSMYRzWv/erda1hM6te7WUY62MWs61jP/vzv/ntda9jL2l
2003 czEhAO/n1oyEc//elDEpGEo5EOfexpyUe+/epefevffvxnNrQpyUStbWzsbGvZyclN7ezmNjWv//
2004 5/f33qWllNbWve/vzv//1ufnve/vvf//xvf3vefnrf//taWlc0pKMf//pbW1Y///jKWlWq2tWsbG
2005 Y///c97eUvf3Ut7nc+/3a87We8bOjOfv1u/37/f//621tb3Gxtbn52Nra87n53uUlJTv/6W9xuf3
2006 /8bW3iExOXu11tbv/5TW/4TO/63e/zmt/1KUxlK1/2u9/wCM/73GzrXG1gBKjACE/87e72NzhCkx
2007 OaXO92OMtUql/xCE/wApUtbe57W9xnN7hHut52Ot/xBSnABKnABavQB7/2ul7zF71gBr77XO73Oc
2008 1lqc9yFSlBApSimE/wAYOQApY0J7zlKM5wAxhABS1gBj/6W95wAhWgA5nAAYSgBS7wBS/wBK9wAp
2009 jABC5wBK/wApnABC/wApxgAhtYSMtQAQYwAp/3OE74SMxgAYxlpjvWNr70pS/wgQ3sbGzs7O1qWl
2010 3qWl70pKe0JC/yEhlCkp/wgI/wAAEAAAIQAAKQAAOQAASgAAUgAAYwAAawAAlAAAnAAApQAArQAA
2011 zgAA1gAA5wAA9wAA/0pC/xgQ52Na9ykhe4R7zikhYxgQSjEpQgAAACwAAAAAPAAoAAAI/wABCBxI
2012 sKDBgwgTKiRIYKHDhxARIvgXsaLFhGgEUBSYoKPHjyBDihxJkuS/kwNLqlzJcuTJjQBaypxpEiVH
2013 mjhxvkyZs2fLnTd9ehxAtKjRo0ZrwhTasUsENhYHKOUpk1E3j11mxCBiQVLEBlJd2owp9iVRjwUs
2014 zMCQ5IcLD4saPVxjIKxIoGTvvqSoyFEFGTBeqEhyxAoSFR/USGKVcEGBAwDshsSr1OYTEyhQpJiS
2015 ZcoUKWOQtJDRJFSaggzUGBgoGSTlsjahlPCRIkWVKT16THHRIoqIISBIEUgAYIGBhgRbf3ytFygU
2016 FZp9UDmxQkkMCRwyZKDBQy4aApABhP8XqNwj88l7BVpQYZtF5iArWgwAgGZBq24HU7OeGhQ90PVA
2017 aKZZCiiUMJ9ArSTEwGqR8ZeXfzbV0MIIMQTBwoUdxDDfAm8sZFyDZVEF4UYSKBEBD0+k6IEFPMxH
2018 3FzldXSea+kBgANJSOWIlIMhXZXAXv+c1WM3PuJEpH8iuhbAkv+MdENPRHaTRkdF/jiWSKCAwlKW
2019 VbbkY5Q0LgUSKExgoYBKCjCxARpdltQNKHaUoYAddnR53lVRnJLKBWh4RIEGCZx5FSOv1OLNDUVe
2020 deZHaWiZAB35fIOGNtbEUeV5oGAByzPOrBPFGt3kwEgxITACSg5oLGGLMg60oQAjaNz/oAAcN4Ai
2021 a0c3kHFDK3jYsw4g9sRzBgPLXdkRrBrQ8gsWQUxCCRZX9IJNBQ1s8IgCdeBCzBYN6IBIN2TUsQYd
2022 dXhDBxdzlAHOHHKEcocZdWwDjx8MTCmjsR2FMAstw1RyiSzHqPLALaOwk8QmzCzDCSi0xJKMMk4E
2023 Yw8389iTDT32GAKOPf7YY0Aa9tATyD3w/EGsefgmgEYUtPiChLKWQDMBJtEUgYkzH2RiTgGfTMCI
2024 Mlu0Yc85hNiDziH2tMqOGL72QY47gshLb7Fi4roELcjoQIsxWpDwQyfS2OCJMkLI4YUmyhgxSTVg
2025 CP2FHPZ80UDcieBjStNPD5LPOyZT/y0iHGiMwswexDSzRiRq6KIMJBc4M8skwKAyChia2KPH3P24
2026 YU8/lFhOTj152OPOHuXMU4g48vCRiN/9rZGLMdS4csUu1JzDgxuipOMDHMKsAwEnq/ByzTrrZMNO
2027 OtO0k84+7KjzBjzplMJOOOOoo8846/ATxqJWinkkGUyEkMAaIezABQM3bMAEK1xEsUMDGjARRxhY
2028 xEGGHfPjEcccca6BRxhyuEMY7FCHMNDhf9140r2qRiVvdENQ3liUArzREW/0qRsRVIAGFfBADnLw
2029 gUSiYASJpMEHhilJTEnhAlGoQqYAZQ1AiqEMZ0jDGtqQImhwwA13yMMevoQAGvGhEAWHGMOAAAA7
2030 """
2031 # The base64 data in the string above was generated by the following procedure:
2032 #
2033 # import base64
2034 # print base64.encodestring(open("fetchmail.gif", "rb").read())
2035 #
2036
2037     # Process options
2038     (options, arguments) = getopt.getopt(sys.argv[1:], "df:hV", ["help",
2039             "version"])
2040     dump = rcfile = None;
2041     for (switch, val) in options:
2042         if (switch == '-d'):
2043             dump = TRUE
2044         elif (switch == '-f'):
2045             rcfile = val
2046         elif (switch == '-h' or switch == '--help'):
2047             print """
2048 Usage: fetchmailconf {[-d] [-f fetchmailrc]|-h|--help|-V|--version}
2049            -d      - dump configuration (for debugging)
2050            -f fmrc - read alternate fetchmailrc file
2051 --help,    -h      - print this help text and quit
2052 --version, -V      - print fetchmailconf version and quit
2053 """
2054             sys.exit(0)
2055         elif (switch == '-V' or switch == '--version'):
2056             print "fetchmailconf %s" % version
2057             print """
2058 Copyright (C) 1997 - 2003 Eric S. Raymond
2059 Copyright (C) 2005 - 2006 Matthias Andree
2060 fetchmailconf comes with ABSOLUTELY NO WARRANTY.  This is free software, you are
2061 welcome to redistribute it under certain conditions.  Please see the file
2062 COPYING in the source or documentation directory for details.  """
2063             sys.exit(0)
2064
2065     # Get client host's FQDN
2066     hostname = socket.gethostbyaddr(socket.gethostname())[0]
2067
2068     # Compute defaults
2069     ConfigurationDefaults = Configuration()
2070     ServerDefaults = Server()
2071     UserDefaults = User()
2072
2073     # Read the existing configuration.  We set the umask to 077 to make sure
2074     # that group & other read/write permissions are shut off -- we wouldn't
2075     # want crackers to snoop password information out of the tempfile.
2076     tmpfile = tempfile.mktemp()
2077     if rcfile:
2078         cmd = "umask 077 && fetchmail </dev/null -f " + rcfile + " --configdump --nosyslog >" + tmpfile
2079     else:
2080         cmd = "umask 077 && fetchmail </dev/null --configdump --nosyslog >" + tmpfile
2081
2082     try:
2083         s = os.system(cmd)
2084         if s != 0:
2085             print "`" + cmd + "' run failure, status " + `s`
2086             raise SystemExit
2087     except:
2088         print "Unknown error while running fetchmail --configdump"
2089         os.remove(tmpfile)
2090         sys.exit(1)
2091
2092     try:
2093         execfile(tmpfile)
2094     except:
2095         print "Can't read configuration output of fetchmail --configdump."
2096         os.remove(tmpfile)
2097         sys.exit(1)
2098
2099     os.remove(tmpfile)
2100
2101     # The tricky part -- initializing objects from the configuration global
2102     # `Configuration' is the top level of the object tree we're going to mung.
2103     # The dictmembers list is used to track the set of fields the dictionary
2104     # contains; in particular, we can use it to tell whether things like the
2105     # monitor, interface, ssl, sslkey, or sslcert fields are present.
2106     dictmembers = []
2107     Fetchmailrc = Configuration()
2108     copy_instance(Fetchmailrc, fetchmailrc)
2109     Fetchmailrc.servers = [];
2110     for server in fetchmailrc['servers']:
2111         Newsite = Server()
2112         copy_instance(Newsite, server)
2113         Fetchmailrc.servers.append(Newsite)
2114         Newsite.users = [];
2115         for user in server['users']:
2116             Newuser = User()
2117             copy_instance(Newuser, user)
2118             Newsite.users.append(Newuser)
2119
2120     # We may want to display the configuration and quit
2121     if dump:
2122         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
2123
2124     # The theory here is that -f alone sets the rcfile location,
2125     # but -d and -f together mean the new configuration should go to stdout.
2126     if not rcfile and not dump:
2127         rcfile = os.environ["HOME"] + "/.fetchmailrc"
2128
2129     # OK, now run the configuration edit
2130     root = MainWindow(rcfile)
2131     root.mainloop()
2132
2133 # The following sets edit modes for GNU EMACS
2134 # Local Variables:
2135 # mode:python
2136 # End: