]> Pileus Git - ~andy/fetchmail/blob - fetchmailconf.py
Offer authorization mode with NTLM/MSN added to expert users.
[~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.48"
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.service != defaultports[self.protocol] and self.service != ianaservices[defaultports[self.protocol]]:
136             res = res + " service " + self.service
137         if self.timeout != ServerDefaults.timeout:
138             res = res + " timeout " + `self.timeout`
139         if self.interval != ServerDefaults.interval:
140             res = res + " interval " + `self.interval`
141         if self.envelope != ServerDefaults.envelope or self.envskip != ServerDefaults.envskip:
142             if self.envskip:
143                 res = res + " envelope " + `self.envskip` + " " + self.envelope
144             else:
145                 res = res + " envelope " + self.envelope
146         if self.qvirtual:
147             res = res + (" qvirtual " + str(self.qvirtual) + "\n");
148         if self.auth != ServerDefaults.auth:
149             res = res + " auth " + self.auth
150         if self.dns != ServerDefaults.dns or self.uidl != ServerDefaults.uidl:
151             res = res + " and options"
152         if self.dns != ServerDefaults.dns:
153             res = res + flag2str(self.dns, 'dns')
154         if self.uidl != ServerDefaults.uidl:
155             res = res + flag2str(self.uidl, 'uidl')
156         if folded:      res = res + "\n    "
157         else:        res = res + " "
158
159         if self.aka:
160              res = res + "aka"
161              for x in self.aka:
162                 res = res + " " + x
163         if self.aka and self.localdomains: res = res + " "
164         if self.localdomains:
165              res = res + ("localdomains")
166              for x in self.localdomains:
167                 res = res + " " + x
168         if (self.aka or self.localdomains):
169             if folded:
170                 res = res + "\n    "
171             else:
172                 res = res + " "
173
174         if self.tracepolls:
175            res = res + "tracepolls\n"
176
177         if self.interface:
178             res = res + " interface " + str(self.interface)
179         if self.monitor:
180             res = res + " monitor " + str(self.monitor)
181         if self.plugin:
182             res = res + " plugin " + `self.plugin`
183         if self.plugout:
184             res = res + " plugout " + `self.plugout`
185         if self.principal:
186             res = res + " principal " + `self.principal`
187         if self.esmtpname:
188             res = res + " esmtpname " + `self.esmtpname`
189         if self.esmtppassword:
190             res = res + " esmtppassword " + `self.esmtppassword`
191         if self.interface or self.monitor or self.principal or self.plugin or self.plugout:
192             if folded:
193                 res = res + "\n"
194
195         if res[-1] == " ": res = res[0:-1]
196
197         for user in self.users:
198             res = res + repr(user)
199         res = res + "\n"
200         return res;
201
202     def __delitem__(self, name):
203         for ui in range(len(self.users)):
204             if self.users[ui].remote == name:
205                 del self.users[ui]
206                 break
207
208     def __repr__(self):
209         return self.dump(TRUE)
210
211     def __str__(self):
212         return "[Server: " + self.dump(FALSE) + "]"
213
214 class User:
215     def __init__(self):
216         if os.environ.has_key("USER"):
217             self.remote = os.environ["USER"]    # Remote username
218         elif os.environ.has_key("LOGNAME"):
219             self.remote = os.environ["LOGNAME"]
220         else:
221             print "Can't get your username!"
222             sys.exit(1)
223         self.localnames = [self.remote,]# Local names
224         self.password = None    # Password for mail account access
225         self.mailboxes = []     # Remote folders to retrieve from
226         self.smtphunt = []      # Hosts to forward to
227         self.fetchdomains = []  # Domains to fetch from
228         self.smtpaddress = None # Append this to MAIL FROM line
229         self.smtpname = None    # Use this for RCPT TO
230         self.preconnect = None  # Connection setup
231         self.postconnect = None # Connection wrapup
232         self.mda = None         # Mail Delivery Agent
233         self.bsmtp = None       # BSMTP output file
234         self.lmtp = FALSE       # Use LMTP rather than SMTP?
235         self.antispam = ""      # Listener's spam-block code
236         self.keep = FALSE       # Keep messages
237         self.flush = FALSE      # Flush messages
238         self.limitflush = FALSE # Flush oversized messages
239         self.fetchall = FALSE   # Fetch old messages
240         self.rewrite = TRUE     # Rewrite message headers
241         self.forcecr = FALSE    # Force LF -> CR/LF
242         self.stripcr = FALSE    # Strip CR
243         self.pass8bits = FALSE  # Force BODY=7BIT
244         self.mimedecode = FALSE # Undo MIME armoring
245         self.dropstatus = FALSE # Drop incoming Status lines
246         self.dropdelivered = FALSE     # Drop incoming Delivered-To lines
247         self.idle = FALSE              # IDLE after poll
248         self.limit = 0          # Message size limit
249         self.warnings = 3600    # Size warning interval (see tunable.h)
250         self.fetchlimit = 0     # Max messages fetched per batch
251         self.fetchsizelimit = 100       # Max message sizes fetched per transaction
252         self.fastuidl = 10      # Do fast uidl 9 out of 10 times
253         self.batchlimit = 0     # Max message forwarded per batch
254         self.expunge = 0        # Interval between expunges (IMAP)
255         self.ssl = 0            # Enable Seccure Socket Layer
256         self.sslkey = None      # SSL key filename
257         self.sslcert = None     # SSL certificate filename
258         self.sslproto = None    # Force SSL?
259         self.sslcertck = 0      # Enable strict SSL cert checking
260         self.sslcertpath = None # Path to trusted certificates
261         self.sslfingerprint = None      # SSL key fingerprint to check
262         self.properties = None  # Extension properties
263         User.typemap = (
264             ('remote',      'String'),
265             # leave out mailboxes and localnames
266             ('password',    'String'),
267             # Leave out smtphunt, fetchdomains
268             ('smtpaddress', 'String'),
269             ('smtpname', 'String'),
270             ('preconnect',  'String'),
271             ('postconnect', 'String'),
272             ('mda',      'String'),
273             ('bsmtp',       'String'),
274             ('lmtp',    'Boolean'),
275             ('antispam',    'String'),
276             ('keep',    'Boolean'),
277             ('flush',       'Boolean'),
278             ('limitflush',  'Boolean'),
279             ('fetchall',    'Boolean'),
280             ('rewrite',     'Boolean'),
281             ('forcecr',     'Boolean'),
282             ('stripcr',     'Boolean'),
283             ('pass8bits',   'Boolean'),
284             ('mimedecode',  'Boolean'),
285             ('dropstatus',  'Boolean'),
286             ('dropdelivered', 'Boolean'),
287             ('idle',    'Boolean'),
288             ('limit',       'Int'),
289             ('warnings',    'Int'),
290             ('fetchlimit',  'Int'),
291             ('fetchsizelimit',  'Int'),
292             ('fastuidl',    'Int'),
293             ('batchlimit',  'Int'),
294             ('expunge',     'Int'),
295             ('ssl',      'Boolean'),
296             ('sslkey',      'String'),
297             ('sslcert',     'String'),
298             ('sslcertck',   'Boolean'),
299             ('sslcertpath', 'String'),
300             ('sslfingerprint', 'String'),
301             ('properties',  'String'))
302
303     def __repr__(self):
304         res = "    "
305         res = res + "user " + `self.remote` + " there ";
306         if self.password:
307             res = res + "with password " + `self.password` + " "
308         if self.localnames:
309             res = res + "is"
310             for x in self.localnames:
311                 res = res + " " + `x`
312             res = res + " here"
313         if (self.keep != UserDefaults.keep
314                 or self.flush != UserDefaults.flush
315                 or self.limitflush != UserDefaults.limitflush
316                 or self.fetchall != UserDefaults.fetchall
317                 or self.rewrite != UserDefaults.rewrite
318                 or self.forcecr != UserDefaults.forcecr
319                 or self.stripcr != UserDefaults.stripcr
320                 or self.pass8bits != UserDefaults.pass8bits
321                 or self.mimedecode != UserDefaults.mimedecode
322                 or self.dropstatus != UserDefaults.dropstatus
323                 or self.dropdelivered != UserDefaults.dropdelivered
324                 or self.idle != UserDefaults.idle):
325             res = res + " options"
326         if self.keep != UserDefaults.keep:
327             res = res + flag2str(self.keep, 'keep')
328         if self.flush != UserDefaults.flush:
329             res = res + flag2str(self.flush, 'flush')
330         if self.limitflush != UserDefaults.limitflush:
331             res = res + flag2str(self.limitflush, 'limitflush')
332         if self.fetchall != UserDefaults.fetchall:
333             res = res + flag2str(self.fetchall, 'fetchall')
334         if self.rewrite != UserDefaults.rewrite:
335             res = res + flag2str(self.rewrite, 'rewrite')
336         if self.forcecr != UserDefaults.forcecr:
337             res = res + flag2str(self.forcecr, 'forcecr')
338         if self.stripcr != UserDefaults.stripcr:
339             res = res + flag2str(self.stripcr, 'stripcr')
340         if self.pass8bits != UserDefaults.pass8bits:
341             res = res + flag2str(self.pass8bits, 'pass8bits')
342         if self.mimedecode != UserDefaults.mimedecode:
343             res = res + flag2str(self.mimedecode, 'mimedecode')
344         if self.dropstatus != UserDefaults.dropstatus:
345             res = res + flag2str(self.dropstatus, 'dropstatus')
346         if self.dropdelivered != UserDefaults.dropdelivered:
347             res = res + flag2str(self.dropdelivered, 'dropdelivered')
348         if self.idle != UserDefaults.idle:
349             res = res + flag2str(self.idle, 'idle')
350         if self.limit != UserDefaults.limit:
351             res = res + " limit " + `self.limit`
352         if self.warnings != UserDefaults.warnings:
353             res = res + " warnings " + `self.warnings`
354         if self.fetchlimit != UserDefaults.fetchlimit:
355             res = res + " fetchlimit " + `self.fetchlimit`
356         if self.fetchsizelimit != UserDefaults.fetchsizelimit:
357             res = res + " fetchsizelimit " + `self.fetchsizelimit`
358         if self.fastuidl != UserDefaults.fastuidl:
359             res = res + " fastuidl " + `self.fastuidl`
360         if self.batchlimit != UserDefaults.batchlimit:
361             res = res + " batchlimit " + `self.batchlimit`
362         if self.ssl and self.ssl != UserDefaults.ssl:
363             res = res + flag2str(self.ssl, 'ssl')
364         if self.sslkey and self.sslkey != UserDefaults.sslkey:
365             res = res + " sslkey " + `self.sslkey`
366         if self.sslcert and self.sslcert != UserDefaults.sslcert:
367             res = res + " sslcert " + `self.sslcert`
368         if self.sslproto and self.sslproto != UserDefaults.sslproto:
369             res = res + " sslproto " + `self.sslproto`
370         if self.sslcertck and self.sslcertck != UserDefaults.sslcertck:
371             res = res +  flag2str(self.sslcertck, 'sslcertck')
372         if self.sslcertpath and self.sslcertpath != UserDefaults.sslcertpath:
373             res = res + " sslcertpath " + `self.sslcertpath`
374         if self.sslfingerprint and self.sslfingerprint != UserDefaults.sslfingerprint:
375             res = res + " sslfingerprint " + `self.sslfingerprint`
376         if self.expunge != UserDefaults.expunge:
377             res = res + " expunge " + `self.expunge`
378         res = res + "\n"
379         trimmed = self.smtphunt;
380         if trimmed != [] and trimmed[len(trimmed) - 1] == "localhost":
381             trimmed = trimmed[0:len(trimmed) - 1]
382         if trimmed != [] and trimmed[len(trimmed) - 1] == hostname:
383             trimmed = trimmed[0:len(trimmed) - 1]
384         if trimmed != []:
385             res = res + "    smtphost "
386             for x in trimmed:
387                 res = res + " " + x
388                 res = res + "\n"
389         trimmed = self.fetchdomains
390         if trimmed != [] and trimmed[len(trimmed) - 1] == hostname:
391             trimmed = trimmed[0:len(trimmed) - 1]
392         if trimmed != []:
393             res = res + "    fetchdomains "
394             for x in trimmed:
395                 res = res + " " + x
396                 res = res + "\n"
397         if self.mailboxes:
398              res = res + "    folder"
399              for x in self.mailboxes:
400                 res = res + " " + x
401              res = res + "\n"
402         for fld in ('smtpaddress', 'preconnect', 'postconnect', 'mda', 'bsmtp', 'properties'):
403             if getattr(self, fld):
404                 res = res + " %s %s\n" % (fld, `getattr(self, fld)`)
405         if self.lmtp != UserDefaults.lmtp:
406             res = res + flag2str(self.lmtp, 'lmtp')
407         if self.antispam != UserDefaults.antispam:
408             res = res + "    antispam " + self.antispam + "\n"
409         return res;
410
411     def __str__(self):
412         return "[User: " + repr(self) + "]"
413
414 #
415 # Helper code
416 #
417
418 # IANA port assignments and bogus 1109 entry
419 ianaservices = {"pop2":109,
420                 "pop3":110,
421                 "1109":1109,
422                 "imap":143,
423                 "smtp":25,
424                 "odmr":366}
425
426 # fetchmail protocol to IANA service name
427 defaultports = {"auto":None,
428                 "POP2":"pop2",
429                 "POP3":"pop3",
430                 "APOP":"pop3",
431                 "KPOP":"1109",
432                 "IMAP":"imap",
433                 "ETRN":"smtp",
434                 "ODMR":"odmr"}
435
436 authlist = ("any", "password", "gssapi", "kerberos", "ssh", "otp",
437         "msn", "ntlm")
438
439 listboxhelp = {
440     'title' : 'List Selection Help',
441     'banner': 'List Selection',
442     'text' : """
443 You must select an item in the list box (by clicking on it).
444 """}
445
446 def flag2str(value, string):
447 # make a string representation of a .fetchmailrc flag or negated flag
448     str = ""
449     if value != None:
450         str = str + (" ")
451         if value == FALSE: str = str + ("no ")
452         str = str + string;
453     return str
454
455 class LabeledEntry(Frame):
456 # widget consisting of entry field with caption to left
457     def bind(self, key, action):
458         self.E.bind(key, action)
459     def focus_set(self):
460         self.E.focus_set()
461     def __init__(self, Master, text, textvar, lwidth, ewidth=12):
462         Frame.__init__(self, Master)
463         self.L = Label(self, {'text':text, 'width':lwidth, 'anchor':'w'})
464         self.E = Entry(self, {'textvar':textvar, 'width':ewidth})
465         self.L.pack({'side':'left'})
466         self.E.pack({'side':'left', 'expand':'1', 'fill':'x'})
467
468 def ButtonBar(frame, legend, ref, alternatives, depth, command):
469 # array of radio buttons, caption to left, picking from a string list
470     bar = Frame(frame)
471     width = (len(alternatives)+1) / depth;
472     Label(bar, text=legend).pack(side=LEFT)
473     for column in range(width):
474         subframe = Frame(bar)
475         for row in range(depth):
476             ind = width * row + column
477             if ind < len(alternatives):
478                 Radiobutton(subframe,
479                         {'text':alternatives[ind],
480                          'variable':ref,
481                          'value':alternatives[ind],
482                          'command':command}).pack(side=TOP, anchor=W)
483             else:
484                 # This is just a spacer
485                 Radiobutton(subframe,
486                         {'text':" ",'state':DISABLED}).pack(side=TOP, anchor=W)
487         subframe.pack(side=LEFT)
488     bar.pack(side=TOP);
489     return bar
490
491 def helpwin(helpdict):
492 # help message window with a self-destruct button
493     helpwin = Toplevel()
494     helpwin.title(helpdict['title'])
495     helpwin.iconname(helpdict['title'])
496     Label(helpwin, text=helpdict['banner']).pack()
497     textframe = Frame(helpwin)
498     scroll = Scrollbar(textframe)
499     helpwin.textwidget = Text(textframe, setgrid=TRUE)
500     textframe.pack(side=TOP, expand=YES, fill=BOTH)
501     helpwin.textwidget.config(yscrollcommand=scroll.set)
502     helpwin.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
503     scroll.config(command=helpwin.textwidget.yview)
504     scroll.pack(side=RIGHT, fill=BOTH)
505     helpwin.textwidget.insert(END, helpdict['text']);
506     Button(helpwin, text='Done',
507            command=lambda x=helpwin: x.destroy(), bd=2).pack()
508     textframe.pack(side=TOP)
509
510 def make_icon_window(base, image):
511     try:
512         # Some older pythons will error out on this
513         icon_image = PhotoImage(data=image)
514         icon_window = Toplevel()
515         Label(icon_window, image=icon_image, bg='black').pack()
516         base.master.iconwindow(icon_window)
517         # Avoid TkInter brain death. PhotoImage objects go out of
518         # scope when the enclosing function returns.  Therefore
519         # we have to explicitly link them to something.
520         base.keepalive.append(icon_image)
521     except:
522         pass
523
524 class ListEdit(Frame):
525 # edit a list of values (duplicates not allowed) with a supplied editor hook
526     def __init__(self, newlegend, list, editor, deletor, master, helptxt):
527         self.editor = editor
528         self.deletor = deletor
529         self.list = list
530
531         # Set up a widget to accept new elements
532         self.newval = StringVar(master)
533         newwin = LabeledEntry(master, newlegend, self.newval, '12')
534         newwin.bind('<Double-1>', self.handleNew)
535         newwin.bind('<Return>', self.handleNew)
536         newwin.pack(side=TOP, fill=X, anchor=E)
537
538         # Edit the existing list
539         listframe = Frame(master)
540         scroll = Scrollbar(listframe)
541         self.listwidget = Listbox(listframe, height=0, selectmode='browse')
542         if self.list:
543             for x in self.list:
544                 self.listwidget.insert(END, x)
545         listframe.pack(side=TOP, expand=YES, fill=BOTH)
546         self.listwidget.config(yscrollcommand=scroll.set)
547         self.listwidget.pack(side=LEFT, expand=YES, fill=BOTH)
548         scroll.config(command=self.listwidget.yview)
549         scroll.pack(side=RIGHT, fill=BOTH)
550         self.listwidget.config(selectmode=SINGLE, setgrid=TRUE)
551         self.listwidget.bind('<Double-1>', self.handleList);
552         self.listwidget.bind('<Return>', self.handleList);
553
554         bf = Frame(master);
555         if self.editor:
556             Button(bf, text='Edit',   command=self.editItem).pack(side=LEFT)
557         Button(bf, text='Delete', command=self.deleteItem).pack(side=LEFT)
558         if helptxt:
559             self.helptxt = helptxt
560             Button(bf, text='Help', fg='blue',
561                    command=self.help).pack(side=RIGHT)
562         bf.pack(fill=X)
563
564     def help(self):
565         helpwin(self.helptxt)
566
567     def handleList(self, event):
568         self.editItem();
569
570     def handleNew(self, event):
571         item = self.newval.get()
572         if item:
573             entire = self.listwidget.get(0, self.listwidget.index('end'));
574             if item and (not entire) or (not item in self.listwidget.get(0, self.listwidget.index('end'))):
575                 self.listwidget.insert('end', item)
576                 if self.list != None: self.list.append(item)
577                 if self.editor:
578                     apply(self.editor, (item,))
579             self.newval.set('')
580
581     def editItem(self):
582         select = self.listwidget.curselection()
583         if not select:
584             helpwin(listboxhelp)
585         else:
586             index = select[0]
587             if index and self.editor:
588                 label = self.listwidget.get(index);
589                 if self.editor:
590                     apply(self.editor, (label,))
591
592     def deleteItem(self):
593         select = self.listwidget.curselection()
594         if not select:
595             helpwin(listboxhelp)
596         else:
597             index = string.atoi(select[0])
598             label = self.listwidget.get(index);
599             self.listwidget.delete(index)
600             if self.list != None:
601                 del self.list[index]
602             if self.deletor != None:
603                 apply(self.deletor, (label,))
604
605 def ConfirmQuit(frame, context):
606     ans = Dialog(frame,
607                  title = 'Quit?',
608                  text = 'Really quit ' + context + ' without saving?',
609                  bitmap = 'question',
610                  strings = ('Yes', 'No'),
611                  default = 1)
612     return ans.num == 0
613
614 def dispose_window(master, legend, help, savelegend='OK'):
615     dispose = Frame(master, relief=RAISED, bd=5)
616     Label(dispose, text=legend).pack(side=TOP,pady=10)
617     Button(dispose, text=savelegend, fg='blue',
618            command=master.save).pack(side=LEFT)
619     Button(dispose, text='Quit', fg='blue',
620            command=master.nosave).pack(side=LEFT)
621     Button(dispose, text='Help', fg='blue',
622            command=lambda x=help: helpwin(x)).pack(side=RIGHT)
623     dispose.pack(fill=X)
624     return dispose
625
626 class MyWidget:
627 # Common methods for Tkinter widgets -- deals with Tkinter declaration
628     def post(self, widgetclass, field):
629         for x in widgetclass.typemap:
630             if x[1] == 'Boolean':
631                 setattr(self, x[0], BooleanVar(self))
632             elif x[1] == 'String':
633                 setattr(self, x[0], StringVar(self))
634             elif x[1] == 'Int':
635                 setattr(self, x[0], IntVar(self))
636             source = getattr(getattr(self, field), x[0])
637             if source:
638                 getattr(self, x[0]).set(source)
639
640     def fetch(self, widgetclass, field):
641         for x in widgetclass.typemap:
642             setattr(getattr(self, field), x[0], getattr(self, x[0]).get())
643
644 #
645 # First, code to set the global fetchmail run controls.
646 #
647
648 configure_novice_help = {
649     'title' : 'Fetchmail novice configurator help',
650     'banner': 'Novice configurator help',
651     'text' : """
652 In the `Novice Configurator Controls' panel, you can:
653
654 Press `Save' to save the new fetchmail configuration you have created.
655 Press `Quit' to exit without saving.
656 Press `Help' to bring up this help message.
657
658 In the `Novice Configuration' panels, you will set up the basic data
659 needed to create a simple fetchmail setup.  These include:
660
661 1. The name of the remote site you want to query.
662
663 2. Your login name on that site.
664
665 3. Your password on that site.
666
667 4. A protocol to use (POP, IMAP, ETRN, etc.)
668
669 5. A polling interval.
670
671 6. Options to fetch old messages as well as new, uor to suppress
672    deletion of fetched message.
673
674 The novice-configuration code will assume that you want to forward mail
675 to a local sendmail listener with no special options.
676 """}
677
678 configure_expert_help = {
679     'title' : 'Fetchmail expert configurator help',
680     'banner': 'Expert configurator help',
681     'text' : """
682 In the `Expert Configurator Controls' panel, you can:
683
684 Press `Save' to save the new fetchmail configuration you have edited.
685 Press `Quit' to exit without saving.
686 Press `Help' to bring up this help message.
687
688 In the `Run Controls' panel, you can set the following options that
689 control how fetchmail runs:
690
691 Poll interval
692         Number of seconds to wait between polls in the background.
693         If zero, fetchmail will run in foreground.
694
695 Logfile
696         If empty, emit progress and error messages to stderr.
697         Otherwise this gives the name of the files to write to.
698         This field is ignored if the "Log to syslog?" option is on.
699
700 Idfile
701         If empty, store seen-message IDs in .fetchids under user's home
702         directory.  If nonempty, use given file name.
703
704 Postmaster
705         Who to send multidrop mail to as a last resort if no address can
706         be matched.  Normally empty; in this case, fetchmail treats the
707         invoking user as the address of last resort unless that user is
708         root.  If that user is root, fetchmail sends to `postmaster'.
709
710 Bounces to sender?
711         If this option is on (the default) error mail goes to the sender.
712         Otherwise it goes to the postmaster.
713
714 Send spam bounces?
715         If this option is on, spam bounces are sent to the sender or
716         postmaster (depending on the "Bounces to sender?" option.  Otherwise,
717         spam bounces are not sent (the default).
718
719 Invisible
720         If false (the default) fetchmail generates a Received line into
721         each message and generates a HELO from the machine it is running on.
722         If true, fetchmail generates no Received line and HELOs as if it were
723         the remote site.
724
725 In the `Remote Mail Configurations' panel, you can:
726
727 1. Enter the name of a new remote mail server you want fetchmail to query.
728
729 To do this, simply enter a label for the poll configuration in the
730 `New Server:' box.  The label should be a DNS name of the server (unless
731 you are using ssh or some other tunneling method and will fill in the `via'
732 option on the site configuration screen).
733
734 2. Change the configuration of an existing site.
735
736 To do this, find the site's label in the listbox and double-click it.
737 This will take you to a site configuration dialogue.
738 """}
739
740
741 class ConfigurationEdit(Frame, MyWidget):
742     def __init__(self, configuration, outfile, master, onexit):
743         self.subwidgets = {}
744         self.configuration = configuration
745         self.outfile = outfile
746         self.container = master
747         self.onexit = onexit
748         ConfigurationEdit.mode_to_help = {
749             'novice':configure_novice_help, 'expert':configure_expert_help
750             }
751
752     def server_edit(self, sitename):
753         self.subwidgets[sitename] = ServerEdit(sitename, self).edit(self.mode, Toplevel())
754
755     def server_delete(self, sitename):
756         try:
757             for user in self.subwidgets.keys():
758                 user.destruct()
759             del self.configuration[sitename]
760         except:
761             pass
762
763     def edit(self, mode):
764         self.mode = mode
765         Frame.__init__(self, self.container)
766         self.master.title('fetchmail ' + self.mode + ' configurator');
767         self.master.iconname('fetchmail ' + self.mode + ' configurator');
768         self.master.protocol('WM_DELETE_WINDOW', self.nosave)
769         self.keepalive = []     # Use this to anchor the PhotoImage object
770         make_icon_window(self, fetchmail_icon)
771         Pack.config(self)
772         self.post(Configuration, 'configuration')
773
774         dispose_window(self,
775                        'Configurator ' + self.mode + ' Controls',
776                        ConfigurationEdit.mode_to_help[self.mode],
777                        'Save')
778
779         gf = Frame(self, relief=RAISED, bd = 5)
780         Label(gf,
781                 text='Fetchmail Run Controls',
782                 bd=2).pack(side=TOP, pady=10)
783
784         df = Frame(gf)
785
786         ff = Frame(df)
787         if self.mode != 'novice':
788             # Set the postmaster
789             log = LabeledEntry(ff, '     Postmaster:', self.postmaster, '14')
790             log.pack(side=RIGHT, anchor=E)
791
792         # Set the poll interval
793         de = LabeledEntry(ff, '     Poll interval:', self.poll_interval, '14')
794         de.pack(side=RIGHT, anchor=E)
795         ff.pack()
796
797         df.pack()
798
799         if self.mode != 'novice':
800             pf = Frame(gf)
801             Checkbutton(pf,
802                 {'text':'Bounces to sender?',
803                 'variable':self.bouncemail,
804                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
805             pf.pack(fill=X)
806
807             sb = Frame(gf)
808             Checkbutton(sb,
809                 {'text':'send spam bounces?',
810                 'variable':self.spambounce,
811                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
812             sb.pack(fill=X)
813
814             sf = Frame(gf)
815             Checkbutton(sf,
816                 {'text':'Log to syslog?',
817                 'variable':self.syslog,
818                 'relief':GROOVE}).pack(side=LEFT, anchor=W)
819             log = LabeledEntry(sf, '     Logfile:', self.logfile, '14')
820             log.pack(side=RIGHT, anchor=E)
821             sf.pack(fill=X)
822
823             Checkbutton(gf,
824                 {'text':'Invisible mode?',
825                 'variable':self.invisible,
826                  'relief':GROOVE}).pack(side=LEFT, anchor=W)
827             # Set the idfile
828             log = LabeledEntry(gf, '     Idfile:', self.idfile, '14')
829             log.pack(side=RIGHT, anchor=E)
830
831         gf.pack(fill=X)
832
833         # Expert mode allows us to edit multiple sites
834         lf = Frame(self, relief=RAISED, bd=5)
835         Label(lf,
836               text='Remote Mail Server Configurations',
837               bd=2).pack(side=TOP, pady=10)
838         ListEdit('New Server:',
839                 map(lambda x: x.pollname, self.configuration.servers),
840                 lambda site, self=self: self.server_edit(site),
841                 lambda site, self=self: self.server_delete(site),
842                 lf, remotehelp)
843         lf.pack(fill=X)
844
845     def destruct(self):
846         for sitename in self.subwidgets.keys():
847             self.subwidgets[sitename].destruct()
848         self.master.destroy()
849         self.onexit()
850
851     def nosave(self):
852         if ConfirmQuit(self, self.mode + " configuration editor"):
853             self.destruct()
854
855     def save(self):
856         for sitename in self.subwidgets.keys():
857             self.subwidgets[sitename].save()
858         self.fetch(Configuration, 'configuration')
859         fm = None
860         if not self.outfile:
861             fm = sys.stdout
862         elif not os.path.isfile(self.outfile) or Dialog(self,
863                  title = 'Overwrite existing run control file?',
864                  text = 'Really overwrite existing run control file?',
865                  bitmap = 'question',
866                  strings = ('Yes', 'No'),
867                  default = 1).num == 0:
868             try:
869                 os.rename(self.outfile, self.outfile + "~")
870             # Pre-1.5.2 compatibility...
871             except os.error:
872                 pass
873             fm = open(self.outfile, 'w')
874         if fm:
875             fm.write("# Configuration created %s by fetchmailconf\n" % time.ctime(time.time()))
876             fm.write(`self.configuration`)
877             if self.outfile:
878                 fm.close()
879             if fm != sys.stdout:
880                 os.chmod(self.outfile, 0600)
881             self.destruct()
882
883 #
884 # Server editing stuff.
885 #
886 remotehelp = {
887     'title' : 'Remote site help',
888     'banner': 'Remote sites',
889     'text' : """
890 When you add a site name to the list here,
891 you initialize an entry telling fetchmail
892 how to poll a new site.
893
894 When you select a sitename (by double-
895 clicking it, or by single-clicking to
896 select and then clicking the Edit button),
897 you will open a window to configure that
898 site.
899 """}
900
901 serverhelp = {
902     'title' : 'Server options help',
903     'banner': 'Server Options',
904     'text' : """
905 The server options screen controls fetchmail
906 options that apply to one of your mailservers.
907
908 Once you have a mailserver configuration set
909 up as you like it, you can select `OK' to
910 store it in the server list maintained in
911 the main configuration window.
912
913 If you wish to discard changes to a server
914 configuration, select `Quit'.
915 """}
916
917 controlhelp = {
918     'title' : 'Run Control help',
919     'banner': 'Run Controls',
920     'text' : """
921 If the `Poll normally' checkbox is on, the host is polled as part of
922 the normal operation of fetchmail when it is run with no arguments.
923 If it is off, fetchmail will only query this host when it is given as
924 a command-line argument.
925
926 The `True name of server' box should specify the actual DNS name
927 to query. By default this is the same as the poll name.
928
929 Normally each host described in the file is queried once each
930 poll cycle. If `Cycles to skip between polls' is greater than 0,
931 that's the number of poll cycles that are skipped between the
932 times this post is actually polled.
933
934 The `Server timeout' is the number of seconds fetchmail will wait
935 for a reply from the mailserver before concluding it is hung and
936 giving up.
937 """}
938
939 protohelp = {
940     'title' : 'Protocol and Port help',
941     'banner': 'Protocol and Port',
942     'text' : """
943 These options control the remote-mail protocol
944 and TCP/IP service port used to query this
945 server.
946
947 If you click the `Probe for supported protocols'
948 button, fetchmail will try to find you the most
949 capable server on the selected host (this will
950 only work if you're conncted to the Internet).
951 The probe only checks for ordinary IMAP and POP
952 protocols; fortunately these are the most
953 frequently supported.
954
955 The `Protocol' button bar offers you a choice of
956 all the different protocols available.  The `auto'
957 protocol is the default mode; it probes the host
958 ports for POP3 and IMAP to see if either is
959 available.
960
961 Normally the TCP/IP service port to use is
962 dictated by the protocol choice.  The `Service'
963 field (only present in expert mode) lets you
964 set a non-standard service (port).
965 """}
966
967 sechelp = {
968     'title' : 'Security option help',
969     'banner': 'Security',
970     'text' : """
971 The 'authorization mode' allows you to choose the
972 mode that fetchmail uses to log in to your server. You
973 can usually leave this at 'any', but you will have to pick
974 'NTLM' and 'MSN' manually for the nonce.
975
976 The 'interface' option allows you to specify a range
977 of IP addresses to monitor for activity.  If these
978 addresses are not active, fetchmail will not poll.
979 Specifying this may protect you from a spoofing attack
980 if your client machine has more than one IP gateway
981 address and some of the gateways are to insecure nets.
982
983 The `monitor' option, if given, specifies the only
984 device through which fetchmail is permitted to connect
985 to servers.  This option may be used to prevent
986 fetchmail from triggering an expensive dial-out if the
987 interface is not already active.
988
989 The `interface' and `monitor' options are available
990 only for Linux and freeBSD systems.  See the fetchmail
991 manual page for details on these.
992
993 The ssl option enables SSL communication with a mailserver
994 supporting Secure Sockets Layer. The sslkey and sslcert options
995 declare key and certificate files for use with SSL.
996 The sslcertck option enables strict checking of SSL server
997 certificates (and sslcertpath gives trusted certificate
998 directory). With sslfingerprint, you can specify a finger-
999 print the server's key is checked against.
1000 """}
1001
1002 multihelp = {
1003     'title' : 'Multidrop option help',
1004     'banner': 'Multidrop',
1005     'text' : """
1006 These options are only useful with multidrop mode.
1007 See the manual page for extended discussion.
1008 """}
1009
1010 suserhelp = {
1011     'title' : 'User list help',
1012     'banner': 'User list',
1013     'text' : """
1014 When you add a user name to the list here,
1015 you initialize an entry telling fetchmail
1016 to poll the site on behalf of the new user.
1017
1018 When you select a username (by double-
1019 clicking it, or by single-clicking to
1020 select and then clicking the Edit button),
1021 you will open a window to configure the
1022 user's options on that site.
1023 """}
1024
1025 class ServerEdit(Frame, MyWidget):
1026     def __init__(self, host, parent):
1027         self.parent = parent
1028         self.server = None
1029         self.subwidgets = {}
1030         for site in parent.configuration.servers:
1031             if site.pollname == host:
1032                 self.server = site
1033         if (self.server == None):
1034                 self.server = Server()
1035                 self.server.pollname = host
1036                 self.server.via = None
1037                 parent.configuration.servers.append(self.server)
1038
1039     def edit(self, mode, master=None):
1040         Frame.__init__(self, master)
1041         Pack.config(self)
1042         self.master.title('Fetchmail host ' + self.server.pollname);
1043         self.master.iconname('Fetchmail host ' + self.server.pollname);
1044         self.post(Server, 'server')
1045         self.makeWidgets(self.server.pollname, mode)
1046         self.keepalive = []     # Use this to anchor the PhotoImage object
1047         make_icon_window(self, fetchmail_icon)
1048 #       self.grab_set()
1049 #       self.focus_set()
1050 #       self.wait_window()
1051         return self
1052
1053     def destruct(self):
1054         for username in self.subwidgets.keys():
1055             self.subwidgets[username].destruct()
1056         del self.parent.subwidgets[self.server.pollname]
1057         self.master.destroy()
1058
1059     def nosave(self):
1060         if ConfirmQuit(self, 'server option editing'):
1061             self.destruct()
1062
1063     def save(self):
1064         self.fetch(Server, 'server')
1065         for username in self.subwidgets.keys():
1066             self.subwidgets[username].save()
1067         self.destruct()
1068
1069     def defaultPort(self):
1070         proto = self.protocol.get()
1071         # Callback to reset the port number whenever the protocol type changes.
1072         # We used to only reset the port if it had a default (zero) value.
1073         # This turns out to be a bad idea especially in Novice mode -- if
1074         # you set POP3 and then set IMAP, the port invisibly remained 110.
1075         # Now we reset unconditionally on the theory that if you're setting
1076         # a custom port number you should be in expert mode and playing
1077         # close enough attention to notice this...
1078         self.service.set(defaultports[proto])
1079         if not proto in ("POP3", "APOP", "KPOP"): self.uidl.state = DISABLED
1080
1081     def user_edit(self, username, mode):
1082         self.subwidgets[username] = UserEdit(username, self).edit(mode, Toplevel())
1083
1084     def user_delete(self, username):
1085         if self.subwidgets.has_key(username):
1086             self.subwidgets[username].destruct()
1087         del self.server[username]
1088
1089     def makeWidgets(self, host, mode):
1090         topwin = dispose_window(self, "Server options for querying " + host, serverhelp)
1091
1092         leftwin = Frame(self);
1093         leftwidth = '25';
1094
1095         if mode != 'novice':
1096             ctlwin = Frame(leftwin, relief=RAISED, bd=5)
1097             Label(ctlwin, text="Run Controls").pack(side=TOP)
1098             Checkbutton(ctlwin, text='Poll ' + host + ' normally?', variable=self.active).pack(side=TOP)
1099             LabeledEntry(ctlwin, 'True name of ' + host + ':',
1100                       self.via, leftwidth).pack(side=TOP, fill=X)
1101             LabeledEntry(ctlwin, 'Cycles to skip between polls:',
1102                       self.interval, leftwidth).pack(side=TOP, fill=X)
1103             LabeledEntry(ctlwin, 'Server timeout (seconds):',
1104                       self.timeout, leftwidth).pack(side=TOP, fill=X)
1105             Button(ctlwin, text='Help', fg='blue',
1106                command=lambda: helpwin(controlhelp)).pack(side=RIGHT)
1107             ctlwin.pack(fill=X)
1108
1109         # Compute the available protocols from the compile-time options
1110         protolist = ['auto']
1111         if 'pop2' in feature_options:
1112             protolist.append("POP2")
1113         if 'pop3' in feature_options:
1114             protolist = protolist + ["POP3", "APOP", "KPOP"]
1115         if 'sdps' in feature_options:
1116             protolist.append("SDPS")
1117         if 'imap' in feature_options:
1118             protolist.append("IMAP")
1119         if 'etrn' in feature_options:
1120             protolist.append("ETRN")
1121         if 'odmr' in feature_options:
1122             protolist.append("ODMR")
1123
1124         protwin = Frame(leftwin, relief=RAISED, bd=5)
1125         Label(protwin, text="Protocol").pack(side=TOP)
1126         ButtonBar(protwin, '',
1127                   self.protocol, protolist, 2,
1128                   self.defaultPort)
1129         if mode != 'novice':
1130             LabeledEntry(protwin, 'On server TCP/IP service:',
1131                       self.service, leftwidth).pack(side=TOP, fill=X)
1132             self.defaultPort()
1133             Checkbutton(protwin,
1134                 text="POP3: track `seen' with client-side UIDLs?",
1135                 variable=self.uidl).pack(side=TOP)
1136         Button(protwin, text='Probe for supported protocols', fg='blue',
1137                command=self.autoprobe).pack(side=LEFT)
1138         Button(protwin, text='Help', fg='blue',
1139                command=lambda: helpwin(protohelp)).pack(side=RIGHT)
1140         protwin.pack(fill=X)
1141
1142         userwin = Frame(leftwin, relief=RAISED, bd=5)
1143         Label(userwin, text="User entries for " + host).pack(side=TOP)
1144         ListEdit("New user: ",
1145                  map(lambda x: x.remote, self.server.users),
1146                  lambda u, m=mode, s=self: s.user_edit(u, m),
1147                  lambda u, s=self: s.user_delete(u),
1148                  userwin, suserhelp)
1149         userwin.pack(fill=X)
1150
1151         leftwin.pack(side=LEFT, anchor=N, fill=X);
1152
1153         if mode != 'novice':
1154             rightwin = Frame(self);
1155
1156             mdropwin = Frame(rightwin, relief=RAISED, bd=5)
1157             Label(mdropwin, text="Multidrop options").pack(side=TOP)
1158             LabeledEntry(mdropwin, 'Envelope address header:',
1159                       self.envelope, '22').pack(side=TOP, fill=X)
1160             LabeledEntry(mdropwin, 'Envelope headers to skip:',
1161                       self.envskip, '22').pack(side=TOP, fill=X)
1162             LabeledEntry(mdropwin, 'Name prefix to strip:',
1163                       self.qvirtual, '22').pack(side=TOP, fill=X)
1164             Checkbutton(mdropwin, text="Enable multidrop DNS lookup?",
1165                     variable=self.dns).pack(side=TOP)
1166             Label(mdropwin, text="DNS aliases").pack(side=TOP)
1167             ListEdit("New alias: ", self.server.aka, None, None, mdropwin, None)
1168             Label(mdropwin, text="Domains to be considered local").pack(side=TOP)
1169             ListEdit("New domain: ",
1170                  self.server.localdomains, None, None, mdropwin, multihelp)
1171             mdropwin.pack(fill=X)
1172
1173             if os_type in ('linux', 'freebsd'):
1174                 secwin = Frame(rightwin, relief=RAISED, bd=5)
1175                 Label(secwin, text="Security").pack(side=TOP)
1176                 # Don't actually let users set this.  KPOP sets it implicitly
1177                 ButtonBar(secwin, 'Authorization mode:',
1178                          self.auth, authlist, 1, None).pack(side=TOP)
1179                 if os_type == 'linux' or os_type == 'freebsd'  or 'interface' in dictmembers:
1180                     LabeledEntry(secwin, 'IP range to check before poll:',
1181                          self.interface, leftwidth).pack(side=TOP, fill=X)
1182                 if os_type == 'linux' or os_type == 'freebsd' or 'monitor' in dictmembers:
1183                     LabeledEntry(secwin, 'Interface to monitor:',
1184                          self.monitor, leftwidth).pack(side=TOP, fill=X)
1185                 # Someday this should handle Kerberos 5 too
1186                 if 'kerberos' in feature_options:
1187                     LabeledEntry(secwin, 'Principal:',
1188                          self.principal, '12').pack(side=TOP, fill=X)
1189                 # ESMTP authentication
1190                 LabeledEntry(secwin, 'ESMTP name:',
1191                              self.esmtpname, '12').pack(side=TOP, fill=X)
1192                 LabeledEntry(secwin, 'ESMTP password:',
1193                              self.esmtppassword, '12').pack(side=TOP, fill=X)
1194                 Button(secwin, text='Help', fg='blue',
1195                        command=lambda: helpwin(sechelp)).pack(side=RIGHT)
1196                 secwin.pack(fill=X)
1197
1198             rightwin.pack(side=LEFT, anchor=N);
1199
1200     def autoprobe(self):
1201         # Note: this only handles case (1) near fetchmail.c:1032
1202         # We're assuming people smart enough to set up ssh tunneling
1203         # won't need autoprobing.
1204         if self.server.via:
1205             realhost = self.server.via
1206         else:
1207             realhost = self.server.pollname
1208         greetline = None
1209         for protocol in ("IMAP","POP3","POP2"):
1210             service = defaultports[protocol]
1211             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1212             try:
1213                 sock.connect((realhost, ianaservices[service]))
1214                 greetline = sock.recv(1024)
1215                 sock.close()
1216             except:
1217                 pass
1218             else:
1219                 break
1220         confwin = Toplevel()
1221         if greetline == None:
1222             title = "Autoprobe of " + realhost + " failed"
1223             confirm = """
1224 Fetchmailconf didn't find any mailservers active.
1225 This could mean the host doesn't support any,
1226 or that your Internet connection is down, or
1227 that the host is so slow that the probe timed
1228 out before getting a response.
1229 """
1230         else:
1231             warnings = ''
1232             # OK, now try to recognize potential problems
1233
1234             if protocol == "POP2":
1235                 warnings = warnings + """
1236 It appears you have somehow found a mailserver running only POP2.
1237 Congratulations.  Have you considered a career in archaeology?
1238
1239 Unfortunately, stock fetchmail binaries don't include POP2 support anymore.
1240 Unless the first line of your fetchmail -V output includes the string "POP2",
1241 you'll have to build it from sources yourself with the configure
1242 switch --enable-POP2.
1243
1244 """
1245
1246 ### POP3 servers start here
1247
1248             if string.find(greetline, "1.003") > 0 or string.find(greetline, "1.004") > 0:
1249                 warnings = warnings + """
1250 This appears to be an old version of the UC Davis POP server.  These are
1251 dangerously unreliable (among other problems, they may drop your mailbox
1252 on the floor if your connection is interrupted during the session).
1253
1254 It is strongly recommended that you find a better POP3 server.  The fetchmail
1255 FAQ includes pointers to good ones.
1256
1257 """
1258             if string.find(greetline, "comcast.net") > 0:
1259                 warnings = warnings + """
1260 The Comcast Maillennium POP3 server only returns the first 80K of a long
1261 message retrieved with TOP. Its response to RETR is normal, so use the
1262 `fetchall' option.
1263
1264 """
1265 # Steve VanDevender <stevev@efn.org> writes:
1266 # The only system I have seen this happen with is cucipop-1.31
1267 # under SunOS 4.1.4.  cucipop-1.31 runs fine on at least Solaris
1268 # 2.x and probably quite a few other systems.  It appears to be a
1269 # bug or bad interaction with the SunOS realloc() -- it turns out
1270 # that internally cucipop does allocate a certain data structure in
1271 # multiples of 16, using realloc() to bump it up to the next
1272 # multiple if it needs more.
1273 #
1274 # The distinctive symptom is that when there are 16 messages in the
1275 # inbox, you can RETR and DELE all 16 messages successfully, but on
1276 # QUIT cucipop returns something like "-ERR Error locking your
1277 # mailbox" and aborts without updating it.
1278 #
1279 # The cucipop banner looks like:
1280 #
1281 # +OK Cubic Circle's v1.31 1998/05/13 POP3 ready <6229000062f95036@wakko>
1282 #
1283             if string.find(greetline, "Cubic Circle") > 0:
1284                 warnings = warnings + """
1285 I see your server is running cucipop.  Better make sure the server box
1286 isn't a SunOS 4.1.4 machine; cucipop tickles a bug in SunOS realloc()
1287 under that version, and doesn't cope with the result gracefully.  Newer
1288 SunOS and Solaris machines run cucipop OK.
1289
1290 Also, some versions of cucipop don't assert an exclusive lock on your
1291 mailbox when it's being queried.  This means that if you have more than
1292 one fetchmail query running against the same mailbox, bad things can happen.
1293 """
1294             if string.find(greetline, "David POP3 Server") > 0:
1295                 warnings = warnings + """
1296 This POP3 server is badly broken.  You should get rid of it -- and the
1297 brain-dead Microsoft operating system it rode in on.
1298
1299 """
1300 # The greeting line on the server known to be buggy is:
1301 # +OK POP3 server ready (running FTGate V2, 2, 1, 0 Jun 21 1999 09:55:01)
1302 #
1303             if string.find(greetline, "FTGate") > 0:
1304                 warnings = warnings + """
1305 This POP server has a weird bug; it says OK twice in response to TOP.
1306 Its response to RETR is normal, so use the `fetchall' option.
1307
1308 """
1309             if string.find(greetline, " geonet.de") > 0:
1310                 warnings = warnings + """
1311 You appear to be using geonet.  As of late 2002, the TOP command on
1312 geonet's POP3 is broken.  Use the fetchall option.
1313
1314 """
1315             if string.find(greetline, "OpenMail") > 0:
1316                 warnings = warnings + """
1317 You appear to be using some version of HP OpenMail.  Many versions of
1318 OpenMail do not process the "TOP" command correctly; the symptom is that
1319 only the header and first line of each message is retrieved.  To work
1320 around this bug, turn on `fetchall' on all user entries associated with
1321 this server.
1322
1323 """
1324             if string.find(greetline, "Escape character is") > 0:
1325                 warnings = warnings + """
1326 Your greeting line looks like it was written by a fetid pile of
1327 camel dung identified to me as `popa3d written by Solar Designer'.
1328 Beware!  The UIDL support in this thing is known to be completely broken,
1329 and other things probably are too.
1330
1331 """
1332             if string.find(greetline, "MercuryP/NLM v1.48") > 0:
1333                 warnings = warnings + """
1334 This is not a POP3 server.  It has delusions of being one, but after
1335 RETR all messages are automatically marked to be deleted.  The only
1336 way to prevent this is to issue an RSET before leaving the server.
1337 Fetchmail does this, but we suspect this is probably broken in lots
1338 of other ways, too.
1339
1340 """
1341             if string.find(greetline, "POP-Max") > 0:
1342                 warnings = warnings + """
1343 The Mail Max POP3 server screws up on mail with attachments.  It
1344 reports the message size with attachments included, but doesn't
1345 download them on a RETR or TOP (this violates the IMAP RFCs).  It also
1346 doesn't implement TOP correctly.  You should get rid of it -- and the
1347 brain-dead NT server it rode in on.
1348
1349 """
1350             if string.find(greetline, "POP3 Server Ready") > 0:
1351                 warnings = warnings + """
1352 Some server that uses this greeting line has been observed to choke on
1353 TOP %d 99999999.  Use the fetchall option. if necessary, to force RETR.
1354
1355 """
1356             if string.find(greetline, "QPOP") > 0:
1357                 warnings = warnings + """
1358 This appears to be a version of Eudora qpopper.  That's good.  Fetchmail
1359 knows all about qpopper.  However, be aware that the 2.53 version of
1360 qpopper does something odd that causes fetchmail to hang with a socket
1361 error on very large messages.  This is probably not a fetchmail bug, as
1362 it has been observed with fetchpop.  The fix is to upgrade to qpopper
1363 3.0beta or a more recent version.  Better yet, switch to IMAP.
1364
1365 """
1366             if string.find(greetline, " sprynet.com") > 0:
1367                 warnings = warnings + """
1368 You appear to be using a SpryNet server.  In mid-1999 it was reported that
1369 the SpryNet TOP command marks messages seen.  Therefore, for proper error
1370 recovery in the event of a line drop, it is strongly recommended that you
1371 turn on `fetchall' on all user entries associated with this server.
1372
1373 """
1374             if string.find(greetline, "TEMS POP3") > 0:
1375                 warnings = warnings + """
1376 Your POP3 server has "TEMS" in its header line.  At least one such
1377 server does not process the "TOP" command correctly; the symptom is
1378 that fetchmail hangs when trying to retrieve mail.  To work around
1379 this bug, turn on `fetchall' on all user entries associated with this
1380 server.
1381
1382 """
1383             if string.find(greetline, " spray.se") > 0:
1384                 warnings = warnings + """
1385 Your POP3 server has "spray.se" in its header line.  In May 2000 at
1386 least one such server did not process the "TOP" command correctly; the
1387 symptom is that messages are treated as headerless.  To work around
1388 this bug, turn on `fetchall' on all user entries associated with this
1389 server.
1390
1391 """
1392             if string.find(greetline, " usa.net") > 0:
1393                 warnings = warnings + """
1394 You appear to be using USA.NET's free mail service.  Their POP3 servers
1395 (at least as of the 2.2 version in use mid-1998) are quite flaky, but
1396 fetchmail can compensate.  They seem to require that fetchall be switched on
1397 (otherwise you won't necessarily see all your mail, not even new mail).
1398 They also botch the TOP command the fetchmail normally uses for retrieval
1399 (it only retrieves about 10 lines rather than the number specified).
1400 Turning on fetchall will disable the use of TOP.
1401
1402 Therefore, it is strongly recommended that you turn on `fetchall' on all
1403 user entries associated with this server.
1404
1405 """
1406             if string.find(greetline, " Novonyx POP3") > 0:
1407                 warnings = warnings + """
1408 Your mailserver is running Novonyx POP3.  This server, at least as of
1409 version 2.17, seems to have problems handling and reporting seen bits.
1410 You may have to use the fetchall option.
1411
1412 """
1413             if string.find(greetline, " IMS POP3") > 0:
1414                 warnings = warnings + """
1415 Some servers issuing the greeting line 'IMS POP3' have been known to
1416 do byte-stuffing incorrectly.  This means that if a message you receive
1417 has a . (period) at start of line, fetchmail will become confused and
1418 probably wedge itself.  (This bug was recorded on IMS POP3 0.86.)
1419
1420 """
1421
1422 ### IMAP servers start here
1423
1424             if string.find(greetline, "GroupWise") > 0:
1425                 warnings = warnings + """
1426 The Novell GroupWise IMAP server would be better named GroupFoolish;
1427 it is (according to the designer of IMAP) unusably broken.  Among
1428 other things, it doesn't include a required content length in its
1429 BODY[TEXT] response.<p>
1430
1431 Fetchmail works around this problem, but we strongly recommend voting
1432 with your dollars for a server that isn't brain-dead.  If you stick
1433 with code as shoddy as GroupWise seems to be, you will probably pay
1434 for it with other problems.<p>
1435
1436 """
1437             if string.find(greetline, "Netscape IMAP4rev1 Service 3.6") > 0:
1438                 warnings = warnings + """
1439 This server violates the RFC2060 requirement that a BODY[TEXT] fetch should
1440 set the messages's Seen flag.  As a result, if you use the keep option the
1441 same messages will be downloaded over and over.
1442
1443 """
1444             if string.find(greetline, "InterChange") > 0:
1445                 warnings = warnings + """
1446
1447 The InterChange IMAP server at release levels below 3.61.08 screws up
1448 on mail with attachments.  It doesn't fetch them if you give it a
1449 BODY[TEXT] request, though it does if you request RFC822.TEXT.
1450 According to the IMAP RFCs and their maintainer these should be
1451 equivalent -- and we can't drop the BODY[TEXT] form because M$
1452 Exchange (quite legally under RFC2062) rejectsit.  The InterChange
1453 folks claim to have fixed this bug in 3.61.08.
1454
1455 """
1456             if string.find(greetline, "Imail") > 0:
1457                 warnings = warnings + """
1458 We've seen a bug report indicating that this IMAP server (at least as of
1459 version 5.0.7) returns an invalid body size for messages with MIME
1460 attachments; the effect is to drop the attachments on the floor.  We
1461 recommend you upgrade to a non-broken IMAP server.
1462
1463 """
1464             if string.find(greetline, "Domino IMAP4") > 0:
1465                 warnings = warnings + """
1466 Your IMAP server appears to be Lotus Domino.  This server, at least up
1467 to version 4.6.2a, has a bug in its generation of MIME boundaries (see
1468 the details in the fetchmail FAQ).  As a result, even MIME aware MUAs
1469 will see attachments as part of the message text.  If your Domino server's
1470 POP3 facility is enabled, we recommend you fall back on it.
1471
1472 """
1473
1474 ### Checks for protocol variants start here
1475
1476             closebrak = string.find(greetline, ">")
1477             if  closebrak > 0 and greetline[closebrak+1] == "\r":
1478                 warnings = warnings + """
1479 It looks like you could use APOP on this server and avoid sending it your
1480 password in clear.  You should talk to the mailserver administrator about
1481 this.
1482
1483 """
1484             if string.find(greetline, "IMAP2bis") > 0:
1485                 warnings = warnings + """
1486 IMAP2bis servers have a minor problem; they can't peek at messages without
1487 marking them seen.  If you take a line hit during the retrieval, the
1488 interrupted message may get left on the server, marked seen.
1489
1490 To work around this, it is recommended that you set the `fetchall'
1491 option on all user entries associated with this server, so any stuck
1492 mail will be retrieved next time around.
1493
1494 To fix this bug, upgrade to an IMAP4 server.  The fetchmail FAQ includes
1495 a pointer to an open-source implementation.
1496
1497 """
1498             if string.find(greetline, "IMAP4rev1") > 0:
1499                 warnings = warnings + """
1500 I see an IMAP4rev1 server.  Excellent.  This is (a) the best kind of
1501 remote-mail server, and (b) the one the fetchmail author uses.  Fetchmail
1502 has therefore been extremely well tested with this class of server.
1503
1504 """
1505             if warnings == '':
1506                 warnings = warnings + """
1507 Fetchmail doesn't know anything special about this server type.
1508
1509 """
1510
1511             # Display success window with warnings
1512             title = "Autoprobe of " + realhost + " succeeded"
1513             confirm = "The " + protocol + " server said:\n\n" + greetline + warnings
1514             self.protocol.set(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:h")
2039     dump = rcfile = None;
2040     for (switch, val) in options:
2041         if (switch == '-d'):
2042             dump = TRUE
2043         elif (switch == '-f'):
2044             rcfile = val
2045         elif (switch == '-h'):
2046             print """
2047 Usage: fetchmailconf [-d] [-f fetchmailrc]
2048 -d       - dump configuration (for debugging)
2049 -f fmrc  - read alternate fetchmailrc file
2050 """
2051             sys.exit(0)
2052
2053     # Get client host's FQDN
2054     hostname = socket.gethostbyaddr(socket.gethostname())[0]
2055
2056     # Compute defaults
2057     ConfigurationDefaults = Configuration()
2058     ServerDefaults = Server()
2059     UserDefaults = User()
2060
2061     # Read the existing configuration.  We set the umask to 077 to make sure
2062     # that group & other read/write permissions are shut off -- we wouldn't
2063     # want crackers to snoop password information out of the tempfile.
2064     tmpfile = tempfile.mktemp()
2065     if rcfile:
2066         cmd = "umask 077 && fetchmail </dev/null -f " + rcfile + " --configdump --nosyslog >" + tmpfile
2067     else:
2068         cmd = "umask 077 && fetchmail </dev/null --configdump --nosyslog >" + tmpfile
2069
2070     try:
2071         s = os.system(cmd)
2072         if s != 0:
2073             print "`" + cmd + "' run failure, status " + `s`
2074             raise SystemExit
2075     except:
2076         print "Unknown error while running fetchmail --configdump"
2077         os.remove(tmpfile)
2078         sys.exit(1)
2079
2080     try:
2081         execfile(tmpfile)
2082     except:
2083         print "Can't read configuration output of fetchmail --configdump."
2084         os.remove(tmpfile)
2085         sys.exit(1)
2086
2087     os.remove(tmpfile)
2088
2089     # The tricky part -- initializing objects from the configuration global
2090     # `Configuration' is the top level of the object tree we're going to mung.
2091     # The dictmembers list is used to track the set of fields the dictionary
2092     # contains; in particular, we can use it to tell whether things like the
2093     # monitor, interface, ssl, sslkey, or sslcert fields are present.
2094     dictmembers = []
2095     Fetchmailrc = Configuration()
2096     copy_instance(Fetchmailrc, fetchmailrc)
2097     Fetchmailrc.servers = [];
2098     for server in fetchmailrc['servers']:
2099         Newsite = Server()
2100         copy_instance(Newsite, server)
2101         Fetchmailrc.servers.append(Newsite)
2102         Newsite.users = [];
2103         for user in server['users']:
2104             Newuser = User()
2105             copy_instance(Newuser, user)
2106             Newsite.users.append(Newuser)
2107
2108     # We may want to display the configuration and quit
2109     if dump:
2110         print "This is a dump of the configuration we read:\n"+`Fetchmailrc`
2111
2112     # The theory here is that -f alone sets the rcfile location,
2113     # but -d and -f together mean the new configuration should go to stdout.
2114     if not rcfile and not dump:
2115         rcfile = os.environ["HOME"] + "/.fetchmailrc"
2116
2117     # OK, now run the configuration edit
2118     root = MainWindow(rcfile)
2119     root.mainloop()
2120
2121 # The following sets edit modes for GNU EMACS
2122 # Local Variables:
2123 # mode:python
2124 # End: