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