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