summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Changes120
-rw-r--r--README250
-rw-r--r--index.html112
-rw-r--r--slime-0.11/Changes120
-rw-r--r--slime-0.11/Makefile47
-rw-r--r--slime-0.11/README250
-rw-r--r--slime-0.11/StringIO2.py159
-rw-r--r--slime-0.11/TODO169
-rw-r--r--slime-0.11/doc/slime-manual.sgml36
-rw-r--r--slime-0.11/helpers.py85
-rw-r--r--slime-0.11/mhlib2.py915
-rw-r--r--slime-0.11/slime5
-rw-r--r--slime-0.11/slime.html112
-rw-r--r--slime-0.11/slime_abstract.py541
-rw-r--r--slime-0.11/slime_draft.py52
-rw-r--r--slime-0.11/slime_folderinfo.py77
-rw-r--r--slime-0.11/slime_mh.py245
-rw-r--r--slime-0.11/slime_pgpmime.py168
-rw-r--r--slime-0.11/slime_root.py77
-rw-r--r--slime-0.11/slime_send.py171
-rw-r--r--slime-0.11/slime_unix.py183
-rw-r--r--slime-0.11/ui.py1058
-rw-r--r--slime-0.11/ui_compose.py96
-rw-r--r--slime-0.11/ui_config.py103
-rw-r--r--slime-0.11/ui_helpers.py155
-rw-r--r--slime-main.jpgbin0 -> 23875 bytes
-rw-r--r--slime-manual.html/ch1.html22
-rw-r--r--slime-manual.html/index.html51
-rw-r--r--slime-msg.jpgbin0 -> 23989 bytes
-rw-r--r--uitools/Alerts.py206
-rw-r--r--uitools/ButtonBox.py77
-rw-r--r--uitools/Cursors.py104
-rw-r--r--uitools/KWDict.py35
-rw-r--r--uitools/LabelEntry.py154
-rw-r--r--uitools/Menu.py245
-rw-r--r--uitools/ProgressBar.py70
-rw-r--r--uitools/README186
-rw-r--r--uitools/Scrolled.py184
-rw-r--r--uitools/Shells.py315
-rw-r--r--uitools/StdDialog.py136
-rw-r--r--uitools/TkDlgWrapper.py268
41 files changed, 7359 insertions, 0 deletions
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..ca73d00
--- /dev/null
+++ b/Changes
@@ -0,0 +1,120 @@
+Version 0.11 (released Mon, 15 Sep 1997 00:36:15 +0300):
+
+ * Added slime_folderinfo.py to CVS tree (and, therefore,
+ the distribution).
+ * Confused notes about new abstract classes for folders and messages.
+
+Version 0.10 (released Sun, 17 Aug 1997 18:49:31 +0300):
+
+ * Fixed bug in rescanning MH subfolders that broke things for
+ the second time the folder was scanned.
+ * Show message count in folder list only for inboxes (speeds
+ things up quite a bit).
+ * Let user edit folder-specific info. At the moment the only
+ available bit for editing is whether a folder should be scanned
+ for new messages, but more will follow.
+ * Remove user's address(es) from recipients in replies.
+ * Composition window ask for conformation for send, abort, etc.
+ * MH folders are only rescanned if the directory has been modified
+ since the previous scan. This makes new message check fast.
+ * helpers.find_folder_by_name caches results.
+ * Made the frequency of new message checks settable.
+ * Main window shows total number of unread messages.
+ * Folder count is updated in main window.
+
+Version 0.9 (released Fri, 15 Aug 1997 00:32:23 +0300):
+
+ * Added support for signature files.
+ * Added support for a default Reply-To header.
+ * Program is now started with the command `slime'.
+ * Added `make install'.
+ * Configuration can be saved. Note: Old version of pyliw had a
+ bug, and a newer version is needed.
+ * Show image/gif inline.
+ * Folders can be opened and closed in the folder list without
+ selecting them, by clicking on them with mouse button 2 (middle
+ one on a three-button mouse).
+ * Replies: Quotes are attributed.
+ * Folder list shows number of messages in each folder.
+ * Separated SlimeFolder.rescan_folder into .rescan_messages and
+ .rescan_subfolders. This speeds up searching for a specific
+ folder (the FCC folder) rather a bit.
+ * Use full pathnames when comparing folder names.
+ * User can enter names of inboxes. These are scanned for new
+ messages at program startup and periodically while running
+ (every five minutes, not yet settable). The user interface is
+ stupid (user must enter filenames), and will be changed
+ in a future version.
+
+Version 0.8 (released Sun, 10 Aug 1997 20:02:36 +0300)
+
+ * Added the beginnings of a manual. Well, a skeleton to hold what
+ will be written later.
+ * PGP passphrase is remembered for N minutes (default is 60).
+ * Added support for an external editor.
+ * Copies of saved messages can be saved to a folder.
+
+Version 0.7 (released Sat, 9 Aug 1997 17:20:04 +0300)
+
+ * Added `clean' target to Makefile.
+ * Modal dialog (YesNoDialog) releases grab immediately after
+ user has selected something.
+ * Added rudimentary configuration menu.
+ * Remove duplicate addresses when creating reply template.
+ * Remove empty headers when sending.
+ * Drafts are removed from folder when sent.
+ * "Compose message" will re-edit saved draft, if one is selected.
+ * Add date to message being sent.
+ * Added rudimentary PGP/MIME signed message creation.
+ * Temporary files are created with secretive permissions.
+
+Version 0.6 (released Sun, 27 Jul 1997 23:09:45 +0300)
+
+ * Beginnings of support to send and reply to mail. Works, but
+ is still quite clumsy. Requires that localhost runs an SMTP
+ server.
+
+Version 0.5 (released Sat, 26 Jul 1997 20:08:08 +0300)
+
+ * Put the Slime homepage under CVS.
+ * Added basic PGP/MIME support. May be specific Exmh messages,
+ but we'll see.
+
+Version 0.4 (released Fri, 25 Jul 1997 10:21:03 +0300)
+
+ * Implemented folder.add_message, copy_message_here, and
+ move_message_here.
+ * Mouse button 3 moves current message to different folder
+ (as in exmh).
+
+Version 0.3 (released Wed, 23 Jul 1997 01:59:01 +0300)
+
+ * Rescan in menu actually rescans.
+ * Identifiers for class-internal use start with underscore.
+ * Show the whole line in message list in bold for new articles.
+ * Exit asks for confirmation.
+ * Ask for permission before committing a folder.
+
+Version 0.2 (released Tue, 22 Jul 1997 01:06:46 +0300)
+
+ * Use References in addition to In-Reply-To when building threads.
+ * Added command button for "Delete" to message window.
+ * Don't display empty "important" headers in message window.
+ * Call them "selected", not "important" headers.
+ * Automatic recognition whether ~/Mail is MH or unix top folder.
+ * Replace a newline in a subject with a space, for folder table
+ of contents purposes.
+ * Format dates correctly, even if they are just two digits.
+ * Message window scrolls by height-2, not height-3 lines.
+ * Subject in window title of message window has maximum length.
+ * Messages try to make sure they can't be used while the folder
+ they're in is closed.
+ * Unix messages use the file handle in the parent folder, instead
+ of keeping a copy for themselves. This way, if the parent wants
+ to close it, the messages will cause errors if they try to use it.
+ * Scrolling up and down one line at a time with cursor keys.
+ * In list of folders, subtrees can now be opened and closed.
+ * Added dirty bit to folder. This allow commit to do nothing, unless
+ necessary.
+ * Added and fixed caching of headers, to speed up threading,
+ among other things.
diff --git a/README b/README
new file mode 100644
index 0000000..41ef3b6
--- /dev/null
+++ b/README
@@ -0,0 +1,250 @@
+README for Stupid little mailer, or Slime for short
+version 0.11
+by Lars Wirzenius, liw+slime@iki.fi
+
+
+At the moment, this file is a random collection of notes for a new
+mailer, code name Slime, which may or may not become a reality.
+
+Note that Slime is very much alpha level code, and is quite fragile.
+Be careful.
+
+Installation and use
+
+ 0. Make sure you have Python (with Tkinter) and Mitch Chapman's
+ UITools package installed, and that Python can find UITools.
+ You can find a link to UITools on the Slime home page. Set
+ the PYTHONPATH environment variable to include the directory
+ where it is installed, if necessary.
+
+ 1. Unpack slime-<version>.tar.gz.
+
+ 2. Run "python ui.py".
+
+ 3. Play with it and watch your mail disappear.
+
+ 4. Report any bugs and ideas to author.
+
+Background: Why a new mailer? What is missing from existing ones?
+
+ I have not been satisfied by existing mailers, because
+ they are not very suitable for large amounts of mail,
+ do not support PGP or MIME well, don't have both a text
+ and an X interface, are too slow, or are otherwise
+ unsuitable. For example, I currently use Exmh, which
+ is good, but slow, and lacks a text interface. Emacs
+ has at least two fairly good mailers, but I won't use
+ Emacs as my editor.
+
+ I could find a fairly good one and improve that, but most
+ good mailers are huge programs, and learning them well
+ enough to improve them significantly is a large task.
+ Some would also require me to learn a new language. For
+ example, Exmh is written in Tcl/Tk, but I refuse to
+ learn Tcl, since I don't like the language.
+
+ Most of my requirements are simple enough to implement,
+ it's just that no mailer happens to satisfy all of them.
+ So, lured by the simplicity and hopefully complete
+ supporting libraries of Python, I'm considering to
+ write yet another mailer.
+
+ That won't surprise my friends. After all, I wrote my
+ own editor, as well. :)
+
+Basic requirements
+
+ * Must be simple to implement, since I can't afford to
+ spend lots of time on this.
+
+ * Fast and efficient, within reason: if it feels fast enough,
+ it's fast enough, the point is not to optimize it for speed.
+
+ * The implementation must be modular, so that support for new
+ mail folder formats, mail retrieval and sending mechanisms,
+ user interfaces, and so on can be easily added.
+
+ I will probably implement only stuff that I need myself,
+ but other people should have the possiblity to extend it.
+
+ * Excellent PGP integration, including PGP/MIME, application/pgp,
+ plain PGP messages, key extraction, fetching missing keys,
+ sending keys as attachments or in mail bodies, and so on.
+ Users should be able to basically ignore PGP support, as long
+ as everything works (but the mailer should scream when a letter
+ has a bad signature, for example).
+
+ * Excellent MIME support, at least as good as Exmh. Attachments
+ and stuff should "just work". The mailer should show unknown
+ content types as well as it can, e.g., by showing all printable
+ characters, at least as an option.
+
+ * Must work for huge amounts of e-mail correspondence, on the order of
+ thousands per day (e.g., via many large-volume mailing lists).
+ This means supporting arbitrary numbers of "inbox" folders
+ (including many APOP or IMAP folders) at the same time, doing
+ real message threading, supporting archiving and full text
+ searches, finding addresses easily, and keeping track of
+ messages that need to be processed still (this is different
+ from being unread).
+
+ * The user interface should be simple to learn and use. Typical
+ operations should be a single mouse click or key press. All
+ dangerous operations (including deletion of mail) should be
+ difficult to invoke by accident. Going through unread
+ mail should happen just by pressing a space bar,
+ even if the mail is in many folders.
+
+ * Configuration must be easy (not just via editing a text file)
+ for users. This means something like the Pine configuration
+ menu. Configuration should not be necessary to use the software,
+ however.
+
+ * It must be possible to view any number of folders and messages
+ at once, and to edit any number of replies at once. A draft
+ folder a la MH must be supported.
+
+ * Must be compatible with other mailers, i.e., not require that
+ users convert their folders to a particular folder format.
+ This makes it possible to use Slime concurrently with another
+ mailer (important during development and when users try it out).
+
+ * Maybe: virtual folders, annotations, links between messages.
+ But these will probably be too much work to implement.
+
+Design
+
+ Slime will be implemented in Python, if at all, and
+ will consist of some number of classes that implement
+ various features. Some of these classes will be abstract,
+ they will just define an interface to a large number
+ of similar objects. For example, there will be some
+ kind of Folder class, which defines the interface to a
+ mail folder, and different subclasses will implement
+ the interface to allow access to mailbox files, MH
+ directories, Maildir directories, IMAP folders, and
+ so on. The rest of the program will then not need to
+ care what the actual folder format is.
+
+Folder hierarchy
+
+ A folder collection, such as ~/Mail for MH, or ~/mail for
+ Pine, is a folder, with no messages, just subfolders.
+ This way the folders in a collection form a natural
+ hierarchy.
+
+ There is a SlimeFolderRoot class that lists all
+ folder collections. The user may add and remove folder
+ collections, represented as subfolders (of course), using
+ some sort of menu selection. An instance of this class
+ is made persistent (loaded at startup, saved at exit).
+
+Status header
+
+ The following is gleaned from mutt sources:
+
+ D = deleted
+ * = flagged
+ r = replied
+ - = read
+ O = old
+ N = new?
+
+ And only one character?
+
+ Unknown status chars:
+
+ R
+ A
+
+
+Redesigned abstract classes
+
+ module is slime.py
+
+ Exception:
+ error = 'slime.error'
+
+ def thread_messages(list_of_messages):
+ def find_folder(folder_name):
+
+ class Folder:
+ def __init__(self, full_name):
+
+ def get_full_name(self):
+ def get_short_name(self):
+
+ def delete(self):
+
+ def lock(self):
+ def unlock(self):
+
+ def scan_messages(self, smart=1):
+ def list_messages(self, choose=None):
+ def create_message(self, file):
+ def copy_message(self, msg):
+ def move_message(self, msg):
+
+ def scan_subfolders(self, smart=1):
+ def list_subfolders(self):
+ def scan_subfolders(self):
+ def create_subfolder(self, relative_name):
+
+ def is_clean(self):
+ def make_dirty(self):
+ def make_clean(self):
+ def commit(self):
+
+ class Message:
+ def __init__(self, folder):
+
+ def forget_headers(self):
+ def read_headers(self):
+
+ def is_dirty(self):
+ def make_clean(self):
+
+ def set_status(self, new_status):
+ def get_status(self):
+ def has_status(self, status):
+ def give_status(self, status):
+ def remove_status(self, status):
+
+ def set_contents(self, file):
+
+ def get_file(self):
+
+ # union of the interfaces of rfc822.Message, mimetools.Message,
+ # and mhlib.Message.
+ ...
+ # or maybe there is a better interface that could be
+ # implemented by scratch to avoid using multiple layers?
+
+ def test():
+ - open folder
+ - count # of messages
+ - list some of the headers of each message
+ - thread messages
+ - delete message
+ - add message
+ - copy/move message to another folder (to each folder type)
+ - have N processes try to modify the folder at once
+ (some create messages, others delete them, some just
+ read the folder)
+
+
+ - communication between different windows?
+
+ class MessageWindow:
+ def __init__(self):
+ def set(self, message):
+
+ class MessageListWindow:
+ def __init__(self):
+ def set(self, list_of_messages):
+ def update(self, message):
+
+ class FolderListWindow:
+ def __init__(self):
+ def set(self, list_of_folders):
+ def update(self, folder):
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..4ffea49
--- /dev/null
+++ b/index.html
@@ -0,0 +1,112 @@
+<html>
+<head>
+<title>Slime - the Stupid little mailer</title>
+</head>
+<body>
+
+<h1>Slime - the Stupid little mailer</h1>
+
+<p>See the <a href="README">README</a> for more info, and <a
+href="Changes">Changes</a>, if you've seen any previous version.
+The <a href="slime-manual.html/index.html">manual</a> is not
+yet written.
+
+<p>Screenshots: <a href="slime-main.jpg">main window</a> (jpeg, 25 kB),
+<a href="slime-msg.jpg">message window</a> (jpeg, 25 kB). These are
+of version 0.2, or something.
+
+<p>You need the <a href="slime.tar.gz">Slime source package</a>,
+my <a href="../programs/pyliw-0.3.tar.gz">pyliw package (version 0.3)</a>,
+Mitch Chapman's <a href="uitools.tar.gz">UITools</a>, and
+<a href="http://www.python.org/">Python</a>. <strong>Note</strong>
+that version 0.9 of Slime needs version 0.3 of pyliw (older versions
+had a bug, now fixed).
+
+
+<h2>A small collection of links</h2>
+
+<p>Message formats:
+<ul>
+<li><a href="http://www.c2.org/~raph/pgpmime.html">PGP/MIME homepage</a>
+<li><a href="http://www.c2.org/~raph/impl.html">PGP/MIME implementation notes</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2015.txt">RFC 2015 (PGP/MIME)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1847.txt">RFC 1847 (Security
+ multiparts for MIME)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc822.txt">RFC 822 (Message
+ format for mail)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1036.txt">RFC 1036 (Message
+ format for news)</a>
+<li><a href="ftp://ftp.zoo.toronto.edu/pub/news.txt.Z">Son-of-1036</a> by
+ Henry Spencer
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1892 (Bounces)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1893 (Enhanced
+ Mail System Status Codes)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1894 (Extensible
+ Message Format for Delivery Status Notifications)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2045.txt">RFC 2045 (MIME I:
+ Message Bodies)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2046.txt">RFC 2046 (MIME II:
+ Media Types)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2047.txt">RFC 2047 (MIME III:
+ Header Extensions for Non-ASCII Text)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2048.txt">RFC 2048 (MIME IV:
+ Registration Procedures)</a>
+<li><a href="http://www.cs.ruu.nl/wais/html/na-dir/mail/mime-faq/.html">MIME
+ FAQ (in HTML)</a>
+</ul>
+
+<p>Protocol related stuff:
+<ul>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc821.txt">RFC 821 (SMTP)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1939.txt">RFC 1939 (POP3)</a>
+<li><a href="http://www.imap.org">IMAP</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc977.txt">RFC (NNTP)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1957.txt">RFC 1957 (Observations
+ on POP implementations)</a>
+</ul>
+
+<p>Mailbox formats
+<ul>
+<li><a href="http://www.qmail.org/qmail-manual-html/man5/mbox.html">Normal Unix mbox</a>
+<li><a href="">(?)MH folders</a>
+<li><a href="http://www.qmail.org/qmail-manual-html/man5/maildir.html">Maildir</a>
+</ul>
+
+<p>Other mailers and mail-related software:
+<ul>
+<li><a href="http://www.washington.edu/pine/tech-notes/">Pine technical
+ notes</a>
+<li><a href="http://www.ifi.uio.no/~larsi/notes/notes.html">GNUS's notes
+ on news</a>
+<li><a href="http://www.mutt.org">Mutt</a>
+<li><a href="http://www.sendmail.org/">Sendmail</a>
+<li><a href="http://www.qmail.org/">Qmail</a>
+<li><a href="">(?)Exim</a>
+<li><a href="">(?)Smail</a>
+<li><a href="">(?)Procmail</a>
+<li><a href="http://andrew2.andrew.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>
+<li><a href=""></a>
+</ul>
+
+<p>Other stuff
+<ul>
+<li><a href="http://ds.internic.net/internet-drafts/draft-dusse-smime-msg-02.txt">S/MIME</a>
+<li><a href="http://www.c2.org/~raph/comparison.html">Comparison between e-mail encryption protocols</a>
+<li><a href="">Heikki Kantola</a>
+<li><a href="http://http.bsd.uchicago.edu/%7Et-pierce/news/">Tim Pierce's
+ news stuff</a>
+<li>Bill Wohler's
+ <a href="http://www.worldtalk.com/html/msg_resources/email_ref.html">Email
+ References</a>
+<li>Project Andrew
+ <a href="http://andrew2.andrew.cmu.edu/cyrus/email/email.html">Email
+ Web Resources</a>
+<li><a href="http://www2.thecia.net/users/rnewman/Good_Netkeeping_Seal">Good
+ Net-Keeping Seal of Approval</a>
+<li><a href="http://www.imc.org/workshop/mail-archive/0364.html">S/MIME insecurity</a>
+<li><a href="http://www.imc.org">Internet Mail Consortium</a>
+<li><a href="http://developer.netscape.com/software/index.html">netscape LDAP</a>
+</ul>
+
+</body>
+</html>
diff --git a/slime-0.11/Changes b/slime-0.11/Changes
new file mode 100644
index 0000000..ca73d00
--- /dev/null
+++ b/slime-0.11/Changes
@@ -0,0 +1,120 @@
+Version 0.11 (released Mon, 15 Sep 1997 00:36:15 +0300):
+
+ * Added slime_folderinfo.py to CVS tree (and, therefore,
+ the distribution).
+ * Confused notes about new abstract classes for folders and messages.
+
+Version 0.10 (released Sun, 17 Aug 1997 18:49:31 +0300):
+
+ * Fixed bug in rescanning MH subfolders that broke things for
+ the second time the folder was scanned.
+ * Show message count in folder list only for inboxes (speeds
+ things up quite a bit).
+ * Let user edit folder-specific info. At the moment the only
+ available bit for editing is whether a folder should be scanned
+ for new messages, but more will follow.
+ * Remove user's address(es) from recipients in replies.
+ * Composition window ask for conformation for send, abort, etc.
+ * MH folders are only rescanned if the directory has been modified
+ since the previous scan. This makes new message check fast.
+ * helpers.find_folder_by_name caches results.
+ * Made the frequency of new message checks settable.
+ * Main window shows total number of unread messages.
+ * Folder count is updated in main window.
+
+Version 0.9 (released Fri, 15 Aug 1997 00:32:23 +0300):
+
+ * Added support for signature files.
+ * Added support for a default Reply-To header.
+ * Program is now started with the command `slime'.
+ * Added `make install'.
+ * Configuration can be saved. Note: Old version of pyliw had a
+ bug, and a newer version is needed.
+ * Show image/gif inline.
+ * Folders can be opened and closed in the folder list without
+ selecting them, by clicking on them with mouse button 2 (middle
+ one on a three-button mouse).
+ * Replies: Quotes are attributed.
+ * Folder list shows number of messages in each folder.
+ * Separated SlimeFolder.rescan_folder into .rescan_messages and
+ .rescan_subfolders. This speeds up searching for a specific
+ folder (the FCC folder) rather a bit.
+ * Use full pathnames when comparing folder names.
+ * User can enter names of inboxes. These are scanned for new
+ messages at program startup and periodically while running
+ (every five minutes, not yet settable). The user interface is
+ stupid (user must enter filenames), and will be changed
+ in a future version.
+
+Version 0.8 (released Sun, 10 Aug 1997 20:02:36 +0300)
+
+ * Added the beginnings of a manual. Well, a skeleton to hold what
+ will be written later.
+ * PGP passphrase is remembered for N minutes (default is 60).
+ * Added support for an external editor.
+ * Copies of saved messages can be saved to a folder.
+
+Version 0.7 (released Sat, 9 Aug 1997 17:20:04 +0300)
+
+ * Added `clean' target to Makefile.
+ * Modal dialog (YesNoDialog) releases grab immediately after
+ user has selected something.
+ * Added rudimentary configuration menu.
+ * Remove duplicate addresses when creating reply template.
+ * Remove empty headers when sending.
+ * Drafts are removed from folder when sent.
+ * "Compose message" will re-edit saved draft, if one is selected.
+ * Add date to message being sent.
+ * Added rudimentary PGP/MIME signed message creation.
+ * Temporary files are created with secretive permissions.
+
+Version 0.6 (released Sun, 27 Jul 1997 23:09:45 +0300)
+
+ * Beginnings of support to send and reply to mail. Works, but
+ is still quite clumsy. Requires that localhost runs an SMTP
+ server.
+
+Version 0.5 (released Sat, 26 Jul 1997 20:08:08 +0300)
+
+ * Put the Slime homepage under CVS.
+ * Added basic PGP/MIME support. May be specific Exmh messages,
+ but we'll see.
+
+Version 0.4 (released Fri, 25 Jul 1997 10:21:03 +0300)
+
+ * Implemented folder.add_message, copy_message_here, and
+ move_message_here.
+ * Mouse button 3 moves current message to different folder
+ (as in exmh).
+
+Version 0.3 (released Wed, 23 Jul 1997 01:59:01 +0300)
+
+ * Rescan in menu actually rescans.
+ * Identifiers for class-internal use start with underscore.
+ * Show the whole line in message list in bold for new articles.
+ * Exit asks for confirmation.
+ * Ask for permission before committing a folder.
+
+Version 0.2 (released Tue, 22 Jul 1997 01:06:46 +0300)
+
+ * Use References in addition to In-Reply-To when building threads.
+ * Added command button for "Delete" to message window.
+ * Don't display empty "important" headers in message window.
+ * Call them "selected", not "important" headers.
+ * Automatic recognition whether ~/Mail is MH or unix top folder.
+ * Replace a newline in a subject with a space, for folder table
+ of contents purposes.
+ * Format dates correctly, even if they are just two digits.
+ * Message window scrolls by height-2, not height-3 lines.
+ * Subject in window title of message window has maximum length.
+ * Messages try to make sure they can't be used while the folder
+ they're in is closed.
+ * Unix messages use the file handle in the parent folder, instead
+ of keeping a copy for themselves. This way, if the parent wants
+ to close it, the messages will cause errors if they try to use it.
+ * Scrolling up and down one line at a time with cursor keys.
+ * In list of folders, subtrees can now be opened and closed.
+ * Added dirty bit to folder. This allow commit to do nothing, unless
+ necessary.
+ * Added and fixed caching of headers, to speed up threading,
+ among other things.
diff --git a/slime-0.11/Makefile b/slime-0.11/Makefile
new file mode 100644
index 0000000..e222167
--- /dev/null
+++ b/slime-0.11/Makefile
@@ -0,0 +1,47 @@
+prefix=/usr/local
+bindir=$(prefix)/bin
+libdir=$(prefix)/lib/slime
+
+# The following is for my private use, not for other people.
+release_root=$(HOME)/public_html/Slime
+
+all:
+ echo foo
+
+install:
+ test -d $(bindir) || mkdir -p $(bindir)
+ test -d $(libdir) || mkdir -p $(libdir)
+ sed 's @slimedir@ $(libdir) g' slime > $(bindir)/slime
+ chmod a+rx $(bindir)/slime
+ cp *.py $(libdir)
+ chmod a+r $(libdir)/*
+ python -c 'import compileall; compileall.main()' $(libdir)
+
+manual:
+ cd doc; debiandoc2html slime-manual.sgml
+
+release:
+ if [ -z "$(version)" ]; then \
+ echo "Use 'make release version=1.2.3' to make a release."; \
+ exit 1; fi
+ rm -rf x
+ mkdir x
+ cd x && tag=version_`echo $(version) | tr . _` && \
+ cvs -q export -d slime-$(version) -r $$tag slime && \
+ tar cf - slime-$(version) | gzip -9 > ../slime-$(version).tar.gz
+ cp slime-$(version).tar.gz $(release_root)/slime.tar.gz
+ cp x/slime-$(version)/README $(release_root)/README
+ cp x/slime-$(version)/Changes $(release_root)/Changes
+ cp x/slime-$(version)/slime.html $(release_root)/index.html
+ cd x/slime-$(version) && $(MAKE) manual && \
+ rm -rf $(release_root)/slime-manual.html && \
+ cp -a doc/slime-manual.html $(release_root)
+ rm -rf x
+
+profile:
+ python -c 'import profile, slime_unix; profile.run("slime_unix.profile_main()", "profile.out")'
+ python -c 'import pstats; p=pstats.Stats("profile.out"); p.strip_dirs().sort_stats("stdname").print_stats(); p.print_callers(); p.print_callees()' > profile.txt
+
+clean:
+ rm -f *.pyc
+ rm -rf doc/slime-manual.html
diff --git a/slime-0.11/README b/slime-0.11/README
new file mode 100644
index 0000000..41ef3b6
--- /dev/null
+++ b/slime-0.11/README
@@ -0,0 +1,250 @@
+README for Stupid little mailer, or Slime for short
+version 0.11
+by Lars Wirzenius, liw+slime@iki.fi
+
+
+At the moment, this file is a random collection of notes for a new
+mailer, code name Slime, which may or may not become a reality.
+
+Note that Slime is very much alpha level code, and is quite fragile.
+Be careful.
+
+Installation and use
+
+ 0. Make sure you have Python (with Tkinter) and Mitch Chapman's
+ UITools package installed, and that Python can find UITools.
+ You can find a link to UITools on the Slime home page. Set
+ the PYTHONPATH environment variable to include the directory
+ where it is installed, if necessary.
+
+ 1. Unpack slime-<version>.tar.gz.
+
+ 2. Run "python ui.py".
+
+ 3. Play with it and watch your mail disappear.
+
+ 4. Report any bugs and ideas to author.
+
+Background: Why a new mailer? What is missing from existing ones?
+
+ I have not been satisfied by existing mailers, because
+ they are not very suitable for large amounts of mail,
+ do not support PGP or MIME well, don't have both a text
+ and an X interface, are too slow, or are otherwise
+ unsuitable. For example, I currently use Exmh, which
+ is good, but slow, and lacks a text interface. Emacs
+ has at least two fairly good mailers, but I won't use
+ Emacs as my editor.
+
+ I could find a fairly good one and improve that, but most
+ good mailers are huge programs, and learning them well
+ enough to improve them significantly is a large task.
+ Some would also require me to learn a new language. For
+ example, Exmh is written in Tcl/Tk, but I refuse to
+ learn Tcl, since I don't like the language.
+
+ Most of my requirements are simple enough to implement,
+ it's just that no mailer happens to satisfy all of them.
+ So, lured by the simplicity and hopefully complete
+ supporting libraries of Python, I'm considering to
+ write yet another mailer.
+
+ That won't surprise my friends. After all, I wrote my
+ own editor, as well. :)
+
+Basic requirements
+
+ * Must be simple to implement, since I can't afford to
+ spend lots of time on this.
+
+ * Fast and efficient, within reason: if it feels fast enough,
+ it's fast enough, the point is not to optimize it for speed.
+
+ * The implementation must be modular, so that support for new
+ mail folder formats, mail retrieval and sending mechanisms,
+ user interfaces, and so on can be easily added.
+
+ I will probably implement only stuff that I need myself,
+ but other people should have the possiblity to extend it.
+
+ * Excellent PGP integration, including PGP/MIME, application/pgp,
+ plain PGP messages, key extraction, fetching missing keys,
+ sending keys as attachments or in mail bodies, and so on.
+ Users should be able to basically ignore PGP support, as long
+ as everything works (but the mailer should scream when a letter
+ has a bad signature, for example).
+
+ * Excellent MIME support, at least as good as Exmh. Attachments
+ and stuff should "just work". The mailer should show unknown
+ content types as well as it can, e.g., by showing all printable
+ characters, at least as an option.
+
+ * Must work for huge amounts of e-mail correspondence, on the order of
+ thousands per day (e.g., via many large-volume mailing lists).
+ This means supporting arbitrary numbers of "inbox" folders
+ (including many APOP or IMAP folders) at the same time, doing
+ real message threading, supporting archiving and full text
+ searches, finding addresses easily, and keeping track of
+ messages that need to be processed still (this is different
+ from being unread).
+
+ * The user interface should be simple to learn and use. Typical
+ operations should be a single mouse click or key press. All
+ dangerous operations (including deletion of mail) should be
+ difficult to invoke by accident. Going through unread
+ mail should happen just by pressing a space bar,
+ even if the mail is in many folders.
+
+ * Configuration must be easy (not just via editing a text file)
+ for users. This means something like the Pine configuration
+ menu. Configuration should not be necessary to use the software,
+ however.
+
+ * It must be possible to view any number of folders and messages
+ at once, and to edit any number of replies at once. A draft
+ folder a la MH must be supported.
+
+ * Must be compatible with other mailers, i.e., not require that
+ users convert their folders to a particular folder format.
+ This makes it possible to use Slime concurrently with another
+ mailer (important during development and when users try it out).
+
+ * Maybe: virtual folders, annotations, links between messages.
+ But these will probably be too much work to implement.
+
+Design
+
+ Slime will be implemented in Python, if at all, and
+ will consist of some number of classes that implement
+ various features. Some of these classes will be abstract,
+ they will just define an interface to a large number
+ of similar objects. For example, there will be some
+ kind of Folder class, which defines the interface to a
+ mail folder, and different subclasses will implement
+ the interface to allow access to mailbox files, MH
+ directories, Maildir directories, IMAP folders, and
+ so on. The rest of the program will then not need to
+ care what the actual folder format is.
+
+Folder hierarchy
+
+ A folder collection, such as ~/Mail for MH, or ~/mail for
+ Pine, is a folder, with no messages, just subfolders.
+ This way the folders in a collection form a natural
+ hierarchy.
+
+ There is a SlimeFolderRoot class that lists all
+ folder collections. The user may add and remove folder
+ collections, represented as subfolders (of course), using
+ some sort of menu selection. An instance of this class
+ is made persistent (loaded at startup, saved at exit).
+
+Status header
+
+ The following is gleaned from mutt sources:
+
+ D = deleted
+ * = flagged
+ r = replied
+ - = read
+ O = old
+ N = new?
+
+ And only one character?
+
+ Unknown status chars:
+
+ R
+ A
+
+
+Redesigned abstract classes
+
+ module is slime.py
+
+ Exception:
+ error = 'slime.error'
+
+ def thread_messages(list_of_messages):
+ def find_folder(folder_name):
+
+ class Folder:
+ def __init__(self, full_name):
+
+ def get_full_name(self):
+ def get_short_name(self):
+
+ def delete(self):
+
+ def lock(self):
+ def unlock(self):
+
+ def scan_messages(self, smart=1):
+ def list_messages(self, choose=None):
+ def create_message(self, file):
+ def copy_message(self, msg):
+ def move_message(self, msg):
+
+ def scan_subfolders(self, smart=1):
+ def list_subfolders(self):
+ def scan_subfolders(self):
+ def create_subfolder(self, relative_name):
+
+ def is_clean(self):
+ def make_dirty(self):
+ def make_clean(self):
+ def commit(self):
+
+ class Message:
+ def __init__(self, folder):
+
+ def forget_headers(self):
+ def read_headers(self):
+
+ def is_dirty(self):
+ def make_clean(self):
+
+ def set_status(self, new_status):
+ def get_status(self):
+ def has_status(self, status):
+ def give_status(self, status):
+ def remove_status(self, status):
+
+ def set_contents(self, file):
+
+ def get_file(self):
+
+ # union of the interfaces of rfc822.Message, mimetools.Message,
+ # and mhlib.Message.
+ ...
+ # or maybe there is a better interface that could be
+ # implemented by scratch to avoid using multiple layers?
+
+ def test():
+ - open folder
+ - count # of messages
+ - list some of the headers of each message
+ - thread messages
+ - delete message
+ - add message
+ - copy/move message to another folder (to each folder type)
+ - have N processes try to modify the folder at once
+ (some create messages, others delete them, some just
+ read the folder)
+
+
+ - communication between different windows?
+
+ class MessageWindow:
+ def __init__(self):
+ def set(self, message):
+
+ class MessageListWindow:
+ def __init__(self):
+ def set(self, list_of_messages):
+ def update(self, message):
+
+ class FolderListWindow:
+ def __init__(self):
+ def set(self, list_of_folders):
+ def update(self, folder):
diff --git a/slime-0.11/StringIO2.py b/slime-0.11/StringIO2.py
new file mode 100644
index 0000000..505f057
--- /dev/null
+++ b/slime-0.11/StringIO2.py
@@ -0,0 +1,159 @@
+# class StringIO implements file-like objects that read/write a
+# string buffer (a.k.a. "memory files").
+#
+# This implements (nearly) all stdio methods.
+#
+# f = StringIO() # ready for writing
+# f = StringIO(buf) # ready for reading
+# f.close() # explicitly release resources held
+# flag = f.isatty() # always false
+# pos = f.tell() # get current position
+# f.seek(pos) # set current position
+# f.seek(pos, mode) # mode 0: absolute; 1: relative; 2: relative to EOF
+# buf = f.read() # read until EOF
+# buf = f.read(n) # read up to n bytes
+# buf = f.readline() # read until end of line ('\n') or EOF
+# list = f.readlines()# list of f.readline() results until EOF
+# f.write(buf) # write at current position
+# f.writelines(list) # for line in list: f.write(line)
+# f.getvalue() # return whole file's contents as a string
+#
+# Notes:
+# - Using a real file is often faster (but less convenient).
+# - fileno() is left unimplemented so that code which uses it triggers
+# an exception early.
+# - Seeking far beyond EOF and then writing will insert real null
+# bytes that occupy space in the buffer.
+# - There's a simple test set (see end of this file).
+
+import string
+
+class StringIO:
+ def __init__(self, buf = ''):
+ self.buf = buf
+ self.len = len(buf)
+ self.buflist = []
+ self.pos = 0
+ self.closed = 0
+ self.softspace = 0
+ def close(self):
+ if not self.closed:
+ self.closed = 1
+ del self.buf, self.pos
+ def isatty(self):
+ return 0
+ def seek(self, pos, mode = 0):
+ if self.buflist:
+ self.buf = self.buf + string.joinfields(self.buflist, '')
+ self.buflist = []
+ if mode == 1:
+ pos = pos + self.pos
+ elif mode == 2:
+ pos = pos + self.len
+ self.pos = max(0, pos)
+ def tell(self):
+ return self.pos
+ def read(self, n = -1):
+ if self.buflist:
+ self.buf = self.buf + string.joinfields(self.buflist, '')
+ self.buflist = []
+ if n < 0:
+ newpos = self.len
+ else:
+ newpos = min(self.pos+n, self.len)
+ r = self.buf[self.pos:newpos]
+ self.pos = newpos
+ return r
+ def readline(self, length = None):
+ if self.buflist:
+ self.buf = self.buf + string.joinfields(self.buflist, '')
+ self.buflist = []
+ i = string.find(self.buf, '\n', self.pos)
+ if i < 0:
+ newpos = self.len
+ else:
+ newpos = i+1
+ if not length is None:
+ if self.pos + length < newpos:
+ newpos = self.pos + length
+ r = self.buf[self.pos:newpos]
+ self.pos = newpos
+ return r
+ def readlines(self):
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ line = self.readline()
+ return lines
+ def write(self, s):
+ if not s: return
+ if self.pos > self.len:
+ self.buflist.append('\0'*(self.pos - self.len))
+ self.len = self.pos
+ newpos = self.pos + len(s)
+ if self.pos < self.len:
+ if self.buflist:
+ self.buf = self.buf + string.joinfields(self.buflist, '')
+ self.buflist = []
+ self.buflist = [self.buf[:self.pos], s, self.buf[newpos:]]
+ self.buf = ''
+ else:
+ self.buflist.append(s)
+ self.len = newpos
+ self.pos = newpos
+ def writelines(self, list):
+ self.write(string.joinfields(list, ''))
+ def flush(self):
+ pass
+ def getvalue(self):
+ if self.buflist:
+ self.buf = self.buf + string.joinfields(self.buflist, '')
+ self.buflist = []
+ return self.buf
+
+
+# A little test suite
+
+def test():
+ import sys
+ if sys.argv[1:]:
+ file = sys.argv[1]
+ else:
+ file = '/etc/passwd'
+ lines = open(file, 'r').readlines()
+ text = open(file, 'r').read()
+ f = StringIO()
+ for line in lines[:-2]:
+ f.write(line)
+ f.writelines(lines[-2:])
+ if f.getvalue() != text:
+ raise RuntimeError, 'write failed'
+ length = f.tell()
+ print 'File length =', length
+ f.seek(len(lines[0]))
+ f.write(lines[1])
+ f.seek(0)
+ print 'First line =', `f.readline()`
+ here = f.tell()
+ line = f.readline()
+ print 'Second line =', `line`
+ f.seek(-len(line), 1)
+ line2 = f.read(len(line))
+ if line != line2:
+ raise RuntimeError, 'bad result after seek back'
+ f.seek(len(line2), 1)
+ list = f.readlines()
+ line = list[-1]
+ f.seek(f.tell() - len(line))
+ line2 = f.read()
+ if line != line2:
+ raise RuntimeError, 'bad result after seek back from EOF'
+ print 'Read', len(list), 'more lines'
+ print 'File length =', f.tell()
+ if f.tell() != length:
+ raise RuntimeError, 'bad length'
+ f.close()
+
+if __name__ == '__main__':
+ test()
diff --git a/slime-0.11/TODO b/slime-0.11/TODO
new file mode 100644
index 0000000..c4bc5c4
--- /dev/null
+++ b/slime-0.11/TODO
@@ -0,0 +1,169 @@
+Slimy things to do, and various notes
+-------------------------------------
+
+Things to add or change before next release:
+
+ * mouse-3 doesn't always work (if there's new mail?); don't use
+ integer indexes into list?
+ * error messages with popups, not pring
+ * rescan the current folder as well, periodically
+ * decrement_unread: only if message is in inbox folder; and other
+ problems leading to -1 unread
+ * put send and abort in compose window further away from each other
+ * msgwin title: just name or addr, not both
+
+Later stuff:
+
+* Misc:
+ * add X-Mailer header?
+ * mark replied messages with special status
+ * Copy, move should be done at commit time, and should update the
+ folder window. Copied/moved should be marked with some other
+ status character then D (C and M, perhaps).
+ * allow editing of signature-file from within slime (new option
+ to Config and ConfigEditor: CONFIG_STRING_LONG, which is same
+ as CONFIG_STRING, but ConfigEditor gives a multiline editor)
+ * speed up threading and other slow operations
+ * _intern_string should make sure it doesn't grow too big
+ * run PGP and other external program async. (allow using slime
+ while they're running, e.g., to kill the external program);
+ ditto for scanning inboxes; separate engine and ui to different
+ processes?
+ * implement locking: folder.open probably shouldn't lock folder (folder
+ is kept open while viewed, so that would prevent new mail from
+ arriving); instead, lock when operations on folder require it (rescan,
+ commit, others?), and raise exception LockException if can't get lock
+ - open imap folder locks it?
+ - some lockfiles should be refreshed every N minutes; what if
+ rescan/commit is slow? even just reading one message might
+ be too slow! use separate thread?
+ - after getting lock, make sure the folder is as we expect it to be
+ * spool should be mailbox
+ * automatic fcc to folder of replied-to message for replies? or always
+ a given folder? or never? (settable)
+ * quote: ignore signature (optional)
+ * long references: remove from middle, not end
+ * installation: ask for and set #!-headers in scripts
+ * address parsing breaks if addresses have commas (and in other ways)
+ * avoid using msg.getfulltext; implement msg.getfile
+ instead; this should reduce memory usage
+ * settable: ask whether to move read mail to received (default=no?)
+ or have an MH-style inbox and an "inc"?
+ * print letters in some nice way
+* Unix folders:
+ * unix mailbox commit rewrites 'From ' lines (and wrongly!);
+ when reading a mailbox, store the From_ lines; invent one for new
+ messages (abstract class should have function to return it so it
+ is kept when moving/copying messages from one mbox to another)
+ * unix mailbox commit: use better filename and make sure it doesn't exist yet
+ * unix mailboxes do not handle messages with identical headers;
+ invent a better way to recognize new messages (just scan
+ after last message? but that doesn't notice deleted ones);
+ just use offset in file?
+ * unix mailbox commit: should check that file hasn't been modified since
+ last scan, to prevent losing stuff
+* MH:
+ * MH copy_message, move_message: if source is also in MH folder, use
+ MH routines (much faster); for other message types, add and use
+ a msg.get_file that returns msg._fp; do _not_ read everything into
+ memory
+ * MH folders don't handle renumbering of folders while program is running;
+ use inode numbers for message identity, not message numbers; this
+ makes it robust against external folder packing
+ * MH: should folder be packed? make it an option later?
+* MIME:
+ * mimify at send: deduce and set character set correctly, and other
+ MIME headers too
+ * decode header mime encodings
+ * handle uuencoded stuff
+ * support other charsets than latin1/usascii
+ * save part to file/pipe
+* PGP:
+ * send pgp/mime signed messages: canonicalize message correctly
+ * support application/pgp (read only), text/plain that begins with
+ (empty lines) and ^---BEGIN PGP; showing and sending
+ * learn about S/MIME
+* User interface:
+ * move, copy message: select folder from menu (in absence of dnd)
+ * keyboard controls should work in both windows
+ * It should be possible to change folders without committing
+ (i.e., to forget changes to folder).
+ * Do cursor magic so that a suitable cursor is set everywhere,
+ and so that a busy cursor is shown, when doing slow stuff.
+ * selection list: move barcursor first
+ * set mouse cursor to clock while busy
+ * allow selecting multiple lines in ftoc, and only allow selecting lines
+ * space should go to next new message, in any folder, if at end of one
+ folder
+ * there should be some key for "go to next new message, anywhere"
+ * handle WM_DELETE_WINDOW protocol
+ self.top.protocol("WM_DELETE_WINDOW", self.deleteWindow)
+ * improve error messages (catch exceptions, popup error message box)
+ * mime multiparts: toggle showing each part separately
+ * horizontal scrollbars in selectionlists (put into ScrolledText)
+ * configuration window:
+ - show threaded or sort by date/subject/subject+date/from
+ (should be settable per ftoc-window as well)
+ - hide only headers known to be unimportant? settable!
+ - have one "all options" window, and make cfgwins scrollable
+ * make subwindows in main window resizable with mouse
+ * make columns in ftoc resizeable by mouse and configurable
+ * make dates in ftoc nicer and configurable
+ * ftoc: more info per message (to, first N lines of message (after
+ quotes))
+ * clone main window
+ * msgwin: optionally do paste-source with quotes, and paste-sink
+ as well
+ * "catchup"
+* Documentation:
+ * start a manual
+ * write description of overall structure (for programmers)
+ * find good specification for "Status:" and "X-Status:" headers
+* Python:
+ * early "SeX" messages from da Silva have bad dates, but should be
+ parseable; Python's getdate doesn't do it, though
+ * pythons getdate doesn't do timezone? runtime patch?
+ * python mailbox.py: of what use is _Mailbox.seek, and does it work
+ at all?
+
+Text mode interface (someone else will have to write it):
+
+ * text interface should have a `really terse' mode for slow
+ modems (<=14k :)
+ * text interface: two windows, one shows a letter, one writes reply
+
+Lunatic ideas for after 1.0:
+
+ * simple inline-html support (incoming)
+ * good printer support
+ * mixmaster support (and other anon mail support)
+ * internal editor should allow creation of MIME messages
+ * picons
+ * change pop/imap password securely from mailer (=> user does not need
+ to manually log into server at all)
+ * put ui and engine in different threads?
+ * address book a la exmh (and Emacs bbdb?): grab addresses from every
+ shown message, and have search tool (ctrl-tab) in mail editor; a
+ trad. alias system as well; grab data to aliases from mail, including
+ signature
+ * have address book integrate into external (non-existing) database of
+ contact info (names, phone numbers, e-mail addresses, etc); grab data
+ from signature
+ * folder specific signatures
+ * grab url's from letters, store in suitable file
+ * show text/html
+ * netiquette stuff: check for too much quotes, etc
+ * spam filter on inbox(es)? (move spam-looking stuff into separate folder,
+ or something); generic filtering?
+ * global operations (based on filters): set/reset flag, delete/copy/forward
+ /whatever flagged mail
+ * user defined shortcuts
+ * reply to many letters at once (quote all of them)
+ * interface for subscribing and unsubscribing to mailing lists
+ * compress folders
+ * killfile and scoring
+ * remove duplicates based on message-id
+ * mailing list (un)subscription tool
+ * mark as read/deleted in all folders, when many inboxes for
+ different lists have the same message
+
diff --git a/slime-0.11/doc/slime-manual.sgml b/slime-0.11/doc/slime-manual.sgml
new file mode 100644
index 0000000..a52c814
--- /dev/null
+++ b/slime-0.11/doc/slime-manual.sgml
@@ -0,0 +1,36 @@
+<!doctype debiandoc system>
+<book>
+
+<title>Stupid Little Mailer: User's Guide
+<author>Lars Wirzenius <email>liw@iki.fi
+<version>Version very-pre-alpha
+
+<abstract>Stupid Little Mailer, or Slime for short, is a mail user
+agent.
+
+<copyright>Copyright &copy; 1997 Lars Wirzenius.
+
+<p>Slime, including this manual, is free software;
+you may redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free
+Software Foundation; either version 2, or (at your option)
+any later version.
+
+<p>This is distributed in the hope that it will be useful,
+but without any warranty; without even the implied warranty of
+merchantability or fitness for a particular purpose. See the
+GNU General Public License for more details.
+
+<p>You should have received a copy of the GNU General
+Public License with your Debian GNU/Linux system, in
+/usr/doc/copyright/GPL, or with the SeX source
+package as the file COPYING. If not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+<toc>
+
+<chapt>Introduction
+
+<p>This manual will be written later.
+
+</book>
diff --git a/slime-0.11/helpers.py b/slime-0.11/helpers.py
new file mode 100644
index 0000000..46a5e11
--- /dev/null
+++ b/slime-0.11/helpers.py
@@ -0,0 +1,85 @@
+import spawn, tempfile, time, os, FCNTL
+
+def run_pgp(path, username, password, options, files, stdin=""):
+ env = {}
+ for key in os.environ.keys():
+ env[key] = os.environ[key]
+ env["PGPPASSFD"] = "0"
+
+ if password:
+ stdin = password + "\n" + stdin
+
+ args = options[:]
+ if username:
+ args.append("+myname=" + username)
+ args = args + files
+
+ return spawn.run(path, stdin=stdin, args=args, env=env)
+
+def read_text_from_named_file(filename):
+ f = open(filename, "r")
+ text = f.read()
+ f.close()
+ return text
+
+def write_text_to_named_file(filename, text):
+ fd = os.open(filename,
+ FCNTL.O_WRONLY | FCNTL.O_CREAT | FCNTL.O_EXCL, 0600)
+ os.write(fd, text)
+ os.close(fd)
+
+def rfc822_date():
+ mon = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ dow = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+
+ if time.daylight:
+ date = time.localtime(time.time())
+ if time.altzone < 0:
+ tzsign = '+'
+ secs = -time.altzone
+ else:
+ tzsign = '-'
+ secs = time.altzone
+ tzhours = secs / (60*60)
+ tzmins = secs % (60*60)
+ else:
+ date = time.gmtime(time.time())
+ tzsign = '+'
+ tzhours = 0
+ tzmins = 0
+
+ return "%s, %2d %s %d %02d:%02d:%02d %c%02d%02d" % \
+ (dow[date[6]], date[2], mon[date[1]], date[0],
+ date[3], date[4], date[5], tzsign, tzhours, tzmins)
+
+_name_to_folder_cache = {}
+
+def clear_name_to_folder_cache():
+ global _name_to_folder_cache
+ _name_to_folder_cache = {}
+
+def find_folder_by_name(folder_name, folder):
+ global _name_to_folder_cache
+ if _name_to_folder_cache.has_key(folder_name):
+ return _name_to_folder_cache[folder_name]
+ return recursive_find_folder_by_name(folder_name, folder)
+
+def recursive_find_folder_by_name(folder_name, folder):
+ global _name_to_folder_cache
+ _name_to_folder_cache[repr(folder)] = folder
+
+ if repr(folder) == folder_name:
+ return folder
+ was_open = folder.is_open()
+ if not was_open:
+ folder.open()
+ folder.rescan_subfolders()
+ f = None
+ for subfolder in folder.list_all_subfolders():
+ f = recursive_find_folder_by_name(folder_name, subfolder)
+ if f:
+ break
+ if not was_open:
+ folder.close()
+ return f
diff --git a/slime-0.11/mhlib2.py b/slime-0.11/mhlib2.py
new file mode 100644
index 0000000..c932441
--- /dev/null
+++ b/slime-0.11/mhlib2.py
@@ -0,0 +1,915 @@
+# MH interface -- purely object-oriented (well, almost)
+#
+# Executive summary:
+#
+# import mhlib
+#
+# mh = mhlib.MH() # use default mailbox directory and profile
+# mh = mhlib.MH(mailbox) # override mailbox location (default from profile)
+# mh = mhlib.MH(mailbox, profile) # override mailbox and profile
+#
+# mh.error(format, ...) # print error message -- can be overridden
+# s = mh.getprofile(key) # profile entry (None if not set)
+# path = mh.getpath() # mailbox pathname
+# name = mh.getcontext() # name of current folder
+# mh.setcontext(name) # set name of current folder
+#
+# list = mh.listfolders() # names of top-level folders
+# list = mh.listallfolders() # names of all folders, including subfolders
+# list = mh.listsubfolders(name) # direct subfolders of given folder
+# list = mh.listallsubfolders(name) # all subfolders of given folder
+#
+# mh.makefolder(name) # create new folder
+# mh.deletefolder(name) # delete folder -- must have no subfolders
+#
+# f = mh.openfolder(name) # new open folder object
+#
+# f.error(format, ...) # same as mh.error(format, ...)
+# path = f.getfullname() # folder's full pathname
+# path = f.getsequencesfilename() # full pathname of folder's sequences file
+# path = f.getmessagefilename(n) # full pathname of message n in folder
+#
+# list = f.listmessages() # list of messages in folder (as numbers)
+# n = f.getcurrent() # get current message
+# f.setcurrent(n) # set current message
+# list = f.parsesequence(seq) # parse msgs syntax into list of messages
+# n = f.getlast() # get last message (0 if no messagse)
+# f.setlast(n) # set last message (internal use only)
+#
+# dict = f.getsequences() # dictionary of sequences in folder {name: list}
+# f.putsequences(dict) # write sequences back to folder
+#
+# f.removemessages(list) # remove messages in list from folder
+# f.refilemessages(list, tofolder) # move messages in list to other folder
+# f.movemessage(n, tofolder, ton) # move one message to a given destination
+# f.copymessage(n, tofolder, ton) # copy one message to a given destination
+#
+# m = f.openmessage(n) # new open message object (costs a file descriptor)
+# m is a derived class of mimetools.Message(rfc822.Message), with:
+# s = m.getheadertext() # text of message's headers
+# s = m.getheadertext(pred) # text of message's headers, filtered by pred
+# s = m.getbodytext() # text of message's body, decoded
+# s = m.getbodytext(0) # text of message's body, not decoded
+#
+# XXX To do, functionality:
+# - annotate messages
+# - create, send messages
+#
+# XXX To do, organization:
+# - move IntSet to separate file
+# - move most Message functionality to module mimetools
+
+
+# Customizable defaults
+
+MH_PROFILE = '~/.mh_profile'
+PATH = '~/Mail'
+MH_SEQUENCES = '.mh_sequences'
+FOLDER_PROTECT = 0700
+
+
+# Imported modules
+
+import os
+import sys
+from stat import ST_NLINK
+import regex
+import string
+import mimetools
+import multifile
+import shutil
+
+
+# Exported constants
+
+Error = 'mhlib.Error'
+
+
+# Class representing a particular collection of folders.
+# Optional constructor arguments are the pathname for the directory
+# containing the collection, and the MH profile to use.
+# If either is omitted or empty a default is used; the default
+# directory is taken from the MH profile if it is specified there.
+
+class MH:
+
+ # Constructor
+ def __init__(self, path = None, profile = None):
+ if not profile: profile = MH_PROFILE
+ self.profile = os.path.expanduser(profile)
+ if not path: path = self.getprofile('Path')
+ if not path: path = PATH
+ if not os.path.isabs(path) and path[0] != '~':
+ path = os.path.join('~', path)
+ path = os.path.expanduser(path)
+ if not os.path.isdir(path): raise Error, 'MH() path not found'
+ self.path = path
+
+ # String representation
+ def __repr__(self):
+ return 'MH(%s, %s)' % (`self.path`, `self.profile`)
+
+ # Routine to print an error. May be overridden by a derived class
+ def error(self, msg, *args):
+ sys.stderr.write('MH error: %s\n' % (msg % args))
+
+ # Return a profile entry, None if not found
+ def getprofile(self, key):
+ return pickline(self.profile, key)
+
+ # Return the path (the name of the collection's directory)
+ def getpath(self):
+ return self.path
+
+ # Return the name of the current folder
+ def getcontext(self):
+ context = pickline(os.path.join(self.getpath(), 'context'),
+ 'Current-Folder')
+ if not context: context = 'inbox'
+ return context
+
+ # Set the name of the current folder
+ def setcontext(self, context):
+ fn = os.path.join(self.getpath(), 'context')
+ f = open(fn, "w")
+ f.write("Current-Folder: %s\n" % context)
+ f.close()
+
+ # Return the names of the top-level folders
+ def listfolders(self):
+ folders = []
+ path = self.getpath()
+ for name in os.listdir(path):
+ if name in (os.curdir, os.pardir): continue
+ fullname = os.path.join(path, name)
+ if os.path.isdir(fullname):
+ folders.append(name)
+ folders.sort()
+ return folders
+
+ # Return the names of the subfolders in a given folder
+ # (prefixed with the given folder name)
+ def listsubfolders(self, name):
+ fullname = os.path.join(self.path, name)
+ # Get the link count so we can avoid listing folders
+ # that have no subfolders.
+ st = os.stat(fullname)
+ nlinks = st[ST_NLINK]
+ if nlinks <= 2:
+ return []
+ subfolders = []
+ subnames = os.listdir(fullname)
+ for subname in subnames:
+ if subname in (os.curdir, os.pardir): continue
+ fullsubname = os.path.join(fullname, subname)
+ if os.path.isdir(fullsubname):
+ name_subname = os.path.join(name, subname)
+ subfolders.append(name_subname)
+ # Stop looking for subfolders when
+ # we've seen them all
+ nlinks = nlinks - 1
+ if nlinks <= 2:
+ break
+ subfolders.sort()
+ return subfolders
+
+ # Return the names of all folders, including subfolders, recursively
+ def listallfolders(self):
+ return self.listallsubfolders('')
+
+ # Return the names of subfolders in a given folder, recursively
+ def listallsubfolders(self, name):
+ fullname = os.path.join(self.path, name)
+ # Get the link count so we can avoid listing folders
+ # that have no subfolders.
+ st = os.stat(fullname)
+ nlinks = st[ST_NLINK]
+ if nlinks <= 2:
+ return []
+ subfolders = []
+ subnames = os.listdir(fullname)
+ for subname in subnames:
+ if subname in (os.curdir, os.pardir): continue
+ if subname[0] == ',' or isnumeric(subname): continue
+ fullsubname = os.path.join(fullname, subname)
+ if os.path.isdir(fullsubname):
+ name_subname = os.path.join(name, subname)
+ subfolders.append(name_subname)
+ if not os.path.islink(fullsubname):
+ subsubfolders = self.listallsubfolders(
+ name_subname)
+ subfolders = subfolders + subsubfolders
+ # Stop looking for subfolders when
+ # we've seen them all
+ nlinks = nlinks - 1
+ if nlinks <= 2:
+ break
+ subfolders.sort()
+ return subfolders
+
+ # Return a new Folder object for the named folder
+ def openfolder(self, name):
+ return Folder(self, name)
+
+ # Create a new folder. This raises os.error if the folder
+ # cannot be created
+ def makefolder(self, name):
+ protect = pickline(self.profile, 'Folder-Protect')
+ if protect and isnumeric(protect):
+ mode = string.atoi(protect, 8)
+ else:
+ mode = FOLDER_PROTECT
+ os.mkdir(os.path.join(self.getpath(), name), mode)
+
+ # Delete a folder. This removes files in the folder but not
+ # subdirectories. If deleting the folder itself fails it
+ # raises os.error
+ def deletefolder(self, name):
+ fullname = os.path.join(self.getpath(), name)
+ for subname in os.listdir(fullname):
+ if subname in (os.curdir, os.pardir): continue
+ fullsubname = os.path.join(fullname, subname)
+ try:
+ os.unlink(fullsubname)
+ except os.error:
+ self.error('%s not deleted, continuing...' %
+ fullsubname)
+ os.rmdir(fullname)
+
+
+# Class representing a particular folder
+
+numericprog = regex.compile('[1-9][0-9]*')
+def isnumeric(str):
+ return numericprog.match(str) == len(str)
+
+class Folder:
+
+ # Constructor
+ def __init__(self, mh, name):
+ self.mh = mh
+ self.name = name
+ if not os.path.isdir(self.getfullname()):
+ raise Error, 'no folder %s' % name
+
+ # String representation
+ def __repr__(self):
+ return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
+
+ # Error message handler
+ def error(self, *args):
+ apply(self.mh.error, args)
+
+ # Return the full pathname of the folder
+ def getfullname(self):
+ return os.path.join(self.mh.path, self.name)
+
+ # Return the full pathname of the folder's sequences file
+ def getsequencesfilename(self):
+ return os.path.join(self.getfullname(), MH_SEQUENCES)
+
+ # Return the full pathname of a message in the folder
+ def getmessagefilename(self, n):
+ return os.path.join(self.getfullname(), str(n))
+
+ # Return list of direct subfolders
+ def listsubfolders(self):
+ return self.mh.listsubfolders(self.name)
+
+ # Return list of all subfolders
+ def listallsubfolders(self):
+ return self.mh.listallsubfolders(self.name)
+
+ # Return the list of messages currently present in the folder.
+ # As a side effect, set self.last to the last message (or 0)
+ def listmessages(self):
+ messages = []
+ for name in os.listdir(self.getfullname()):
+ if name[0] != "," and \
+ numericprog.match(name) == len(name):
+ messages.append(string.atoi(name))
+ messages.sort()
+ if messages:
+ self.last = max(messages)
+ else:
+ self.last = 0
+ return messages
+
+ # Return the set of sequences for the folder
+ def getsequences(self):
+ sequences = {}
+ fullname = self.getsequencesfilename()
+ try:
+ f = open(fullname, 'r')
+ except IOError:
+ return sequences
+ while 1:
+ line = f.readline()
+ if not line: break
+ fields = string.splitfields(line, ':')
+ if len(fields) <> 2:
+ self.error('bad sequence in %s: %s' %
+ (fullname, string.strip(line)))
+ key = string.strip(fields[0])
+ value = IntSet(string.strip(fields[1]), ' ').tolist()
+ sequences[key] = value
+ return sequences
+
+ # Write the set of sequences back to the folder
+ def putsequences(self, sequences):
+ fullname = self.getsequencesfilename()
+ f = None
+ for key in sequences.keys():
+ s = IntSet('', ' ')
+ s.fromlist(sequences[key])
+ if not f: f = open(fullname, 'w')
+ f.write('%s: %s\n' % (key, s.tostring()))
+ if not f:
+ try:
+ os.unlink(fullname)
+ except os.error:
+ pass
+ else:
+ f.close()
+
+ # Return the current message. Raise KeyError when there is none
+ def getcurrent(self):
+ return min(self.getsequences()['cur'])
+
+ # Set the current message
+ def setcurrent(self, n):
+ updateline(self.getsequencesfilename(), 'cur', str(n), 0)
+
+ # Parse an MH sequence specification into a message list.
+ def parsesequence(self, seq):
+ if seq == "all":
+ return self.listmessages()
+ try:
+ n = string.atoi(seq, 10)
+ except string.atoi_error:
+ n = 0
+ if n > 0:
+ return [n]
+ if regex.match("^last:", seq) >= 0:
+ try:
+ n = string.atoi(seq[5:])
+ except string.atoi_error:
+ n = 0
+ if n > 0:
+ return self.listmessages()[-n:]
+ if regex.match("^first:", seq) >= 0:
+ try:
+ n = string.atoi(seq[6:])
+ except string.atoi_error:
+ n = 0
+ if n > 0:
+ return self.listmessages()[:n]
+ dict = self.getsequences()
+ if dict.has_key(seq):
+ return dict[seq]
+ context = self.mh.getcontext()
+ # Complex syntax -- use pick
+ pipe = os.popen("pick +%s %s 2>/dev/null" % (self.name, seq))
+ data = pipe.read()
+ sts = pipe.close()
+ self.mh.setcontext(context)
+ if sts:
+ raise Error, "unparseable sequence %s" % `seq`
+ items = string.split(data)
+ return map(string.atoi, items)
+
+ # Open a message -- returns a Message object
+ def openmessage(self, n):
+ return Message(self, n)
+
+ # Remove one or more messages -- may raise os.error
+ def removemessages(self, list):
+ errors = []
+ deleted = []
+ for n in list:
+ path = self.getmessagefilename(n)
+ commapath = self.getmessagefilename(',' + str(n))
+ try:
+ os.unlink(commapath)
+ except os.error:
+ pass
+ try:
+ os.rename(path, commapath)
+ except os.error, msg:
+ errors.append(msg)
+ else:
+ deleted.append(n)
+ if deleted:
+ self.removefromallsequences(deleted)
+ if errors:
+ if len(errors) == 1:
+ raise os.error, errors[0]
+ else:
+ raise os.error, ('multiple errors:', errors)
+
+ # Refile one or more messages -- may raise os.error.
+ # 'tofolder' is an open folder object
+ def refilemessages(self, list, tofolder, keepsequences=0):
+ errors = []
+ refiled = {}
+ for n in list:
+ ton = tofolder.getlast() + 1
+ path = self.getmessagefilename(n)
+ topath = tofolder.getmessagefilename(ton)
+ try:
+ os.rename(path, topath)
+ except os.error:
+ # Try copying
+ try:
+ shutil.copy2(path, topath)
+ os.unlink(path)
+ except (IOError, os.error), msg:
+ errors.append(msg)
+ try:
+ os.unlink(topath)
+ except os.error:
+ pass
+ continue
+ tofolder.setlast(ton)
+ refiled[n] = ton
+ if refiled:
+ if keepsequences:
+ tofolder._copysequences(self, refiled.items())
+ self.removefromallsequences(refiled.keys())
+ if errors:
+ if len(errors) == 1:
+ raise os.error, errors[0]
+ else:
+ raise os.error, ('multiple errors:', errors)
+
+ # Helper for refilemessages() to copy sequences
+ def _copysequences(self, fromfolder, refileditems):
+ fromsequences = fromfolder.getsequences()
+ tosequences = self.getsequences()
+ changed = 0
+ for name, seq in fromsequences.items():
+ try:
+ toseq = tosequences[name]
+ new = 0
+ except:
+ toseq = []
+ new = 1
+ for fromn, ton in refileditems:
+ if fromn in seq:
+ toseq.append(ton)
+ changed = 1
+ if new and toseq:
+ tosequences[name] = toseq
+ if changed:
+ self.putsequences(tosequences)
+
+ # Move one message over a specific destination message,
+ # which may or may not already exist.
+ def movemessage(self, n, tofolder, ton):
+ path = self.getmessagefilename(n)
+ # Open it to check that it exists
+ f = open(path)
+ f.close()
+ del f
+ topath = tofolder.getmessagefilename(ton)
+ backuptopath = tofolder.getmessagefilename(',%d' % ton)
+ try:
+ os.rename(topath, backuptopath)
+ except os.error:
+ pass
+ try:
+ os.rename(path, topath)
+ except os.error:
+ # Try copying
+ ok = 0
+ try:
+ tofolder.setlast(None)
+ shutil.copy2(path, topath)
+ ok = 1
+ finally:
+ if not ok:
+ try:
+ os.unlink(topath)
+ except os.error:
+ pass
+ os.unlink(path)
+ self.removefromallsequences([n])
+
+ # Copy one message over a specific destination message,
+ # which may or may not already exist.
+ def copymessage(self, n, tofolder, ton):
+ path = self.getmessagefilename(n)
+ # Open it to check that it exists
+ f = open(path)
+ f.close()
+ del f
+ topath = tofolder.getmessagefilename(ton)
+ backuptopath = tofolder.getmessagefilename(',%d' % ton)
+ try:
+ os.rename(topath, backuptopath)
+ except os.error:
+ pass
+ ok = 0
+ try:
+ tofolder.setlast(None)
+ shutil.copy2(path, topath)
+ ok = 1
+ finally:
+ if not ok:
+ try:
+ os.unlink(topath)
+ except os.error:
+ pass
+
+ # Create a message, with text from the open file txt.
+ def createmessage(self, n, txt):
+ path = self.getmessagefilename(n)
+ backuppath = self.getmessagefilename(',%d' % n)
+ try:
+ os.rename(path, backuppath)
+ except os.error:
+ pass
+ ok = 0
+ try:
+ f = open(path, "w")
+ while 1:
+ buf = txt.read(16*1024)
+ if not buf:
+ break
+ f.write(buf)
+ f.close()
+ ok = 1
+ finally:
+ if not ok:
+ try:
+ os.unlink(path)
+ except os.error:
+ pass
+
+ # Remove one or more messages from all sequeuces (including last)
+ def removefromallsequences(self, list):
+ if hasattr(self, 'last') and self.last in list:
+ del self.last
+ sequences = self.getsequences()
+ changed = 0
+ for name, seq in sequences.items():
+ for n in list:
+ if n in seq:
+ seq.remove(n)
+ changed = 1
+ if not seq:
+ del sequences[name]
+ if changed:
+ self.putsequences(sequences)
+
+ # Return the last message number
+ def getlast(self):
+ if not hasattr(self, 'last'):
+ messages = self.listmessages()
+ return self.last
+
+ # Set the last message number
+ def setlast(self, last):
+ if last is None:
+ if hasattr(self, 'last'):
+ del self.last
+ else:
+ self.last = last
+
+class Message(mimetools.Message):
+
+ # Constructor
+ def __init__(self, f, n, fp = None):
+ self.folder = f
+ self.number = n
+ if not fp:
+ path = f.getmessagefilename(n)
+ fp = open(path, 'r')
+ mimetools.Message.__init__(self, fp)
+
+ # String representation
+ def __repr__(self):
+ return 'Message(%s, %s)' % (repr(self.folder), self.number)
+
+ # Return the message's header text as a string. If an
+ # argument is specified, it is used as a filter predicate to
+ # decide which headers to return (its argument is the header
+ # name converted to lower case).
+ def getheadertext(self, pred = None):
+ if not pred:
+ return string.joinfields(self.headers, '')
+ headers = []
+ hit = 0
+ for line in self.headers:
+ if line[0] not in string.whitespace:
+ i = string.find(line, ':')
+ if i > 0:
+ hit = pred(string.lower(line[:i]))
+ if hit: headers.append(line)
+ return string.joinfields(headers, '')
+
+ # Return the message's body text as string. This undoes a
+ # Content-Transfer-Encoding, but does not interpret other MIME
+ # features (e.g. multipart messages). To suppress to
+ # decoding, pass a 0 as argument
+ def getbodytext(self, decode = 1):
+ self.fp.seek(self.startofbody)
+ encoding = self.getencoding()
+ if not decode or encoding in ('7bit', '8bit', 'binary'):
+ return self.fp.read()
+ from StringIO import StringIO
+ output = StringIO()
+ mimetools.decode(self.fp, output, encoding)
+ return output.getvalue()
+
+ # Only for multipart messages: return the message's body as a
+ # list of SubMessage objects. Each submessage object behaves
+ # (almost) as a Message object.
+ def getbodyparts(self):
+ if self.getmaintype() != 'multipart':
+ raise Error, \
+ 'Content-Type is not multipart/*'
+ bdry = self.getparam('boundary')
+ if not bdry:
+ raise Error, 'multipart/* without boundary param'
+ self.fp.seek(self.startofbody)
+ mf = multifile.MultiFile(self.fp)
+ mf.push(bdry)
+ parts = []
+ while mf.next():
+ n = str(self.number) + '.' + `1 + len(parts)`
+ part = SubMessage(self.folder, n, mf)
+ parts.append(part)
+ mf.pop()
+ return parts
+
+ # Return body, either a string or a list of messages
+ def getbody(self):
+ if self.getmaintype() == 'multipart':
+ return self.getbodyparts()
+ else:
+ return self.getbodytext()
+
+
+class SubMessage(Message):
+
+ # Constructor
+ def __init__(self, f, n, fp):
+ Message.__init__(self, f, n, fp)
+ if self.getmaintype() == 'multipart':
+ self.body = Message.getbodyparts(self)
+ else:
+ self.body = Message.getbodytext(self)
+ # XXX If this is big, should remember file pointers
+
+ # String representation
+ def __repr__(self):
+ f, n, fp = self.folder, self.number, self.fp
+ return 'SubMessage(%s, %s, %s)' % (f, n, fp)
+
+ def getbodytext(self):
+ if type(self.body) == type(''):
+ return self.body
+
+ def getbodyparts(self):
+ if type(self.body) == type([]):
+ return self.body
+
+ def getbody(self):
+ return self.body
+
+
+# Class implementing sets of integers.
+#
+# This is an efficient representation for sets consisting of several
+# continuous ranges, e.g. 1-100,200-400,402-1000 is represented
+# internally as a list of three pairs: [(1,100), (200,400),
+# (402,1000)]. The internal representation is always kept normalized.
+#
+# The constructor has up to three arguments:
+# - the string used to initialize the set (default ''),
+# - the separator between ranges (default ',')
+# - the separator between begin and end of a range (default '-')
+# The separators may be regular expressions and should be different.
+#
+# The tostring() function yields a string that can be passed to another
+# IntSet constructor; __repr__() is a valid IntSet constructor itself.
+#
+# XXX The default begin/end separator means that negative numbers are
+# not supported very well.
+#
+# XXX There are currently no operations to remove set elements.
+
+class IntSet:
+
+ def __init__(self, data = None, sep = ',', rng = '-'):
+ self.pairs = []
+ self.sep = sep
+ self.rng = rng
+ if data: self.fromstring(data)
+
+ def reset(self):
+ self.pairs = []
+
+ def __cmp__(self, other):
+ return cmp(self.pairs, other.pairs)
+
+ def __hash__(self):
+ return hash(self.pairs)
+
+ def __repr__(self):
+ return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
+ `self.sep`, `self.rng`)
+
+ def normalize(self):
+ self.pairs.sort()
+ i = 1
+ while i < len(self.pairs):
+ alo, ahi = self.pairs[i-1]
+ blo, bhi = self.pairs[i]
+ if ahi >= blo-1:
+ self.pairs[i-1:i+1] = [
+ (alo, max(ahi, bhi))]
+ else:
+ i = i+1
+
+ def tostring(self):
+ s = ''
+ for lo, hi in self.pairs:
+ if lo == hi: t = `lo`
+ else: t = `lo` + self.rng + `hi`
+ if s: s = s + (self.sep + t)
+ else: s = t
+ return s
+
+ def tolist(self):
+ l = []
+ for lo, hi in self.pairs:
+ m = range(lo, hi+1)
+ l = l + m
+ return l
+
+ def fromlist(self, list):
+ for i in list:
+ self.append(i)
+
+ def clone(self):
+ new = IntSet()
+ new.pairs = self.pairs[:]
+ return new
+
+ def min(self):
+ return self.pairs[0][0]
+
+ def max(self):
+ return self.pairs[-1][-1]
+
+ def contains(self, x):
+ for lo, hi in self.pairs:
+ if lo <= x <= hi: return 1
+ return 0
+
+ def append(self, x):
+ for i in range(len(self.pairs)):
+ lo, hi = self.pairs[i]
+ if x < lo: # Need to insert before
+ if x+1 == lo:
+ self.pairs[i] = (x, hi)
+ else:
+ self.pairs.insert(i, (x, x))
+ if i > 0 and x-1 == self.pairs[i-1][1]:
+ # Merge with previous
+ self.pairs[i-1:i+1] = [
+ (self.pairs[i-1][0],
+ self.pairs[i][1])
+ ]
+ return
+ if x <= hi: # Already in set
+ return
+ i = len(self.pairs) - 1
+ if i >= 0:
+ lo, hi = self.pairs[i]
+ if x-1 == hi:
+ self.pairs[i] = lo, x
+ return
+ self.pairs.append((x, x))
+
+ def addpair(self, xlo, xhi):
+ if xlo > xhi: return
+ self.pairs.append((xlo, xhi))
+ self.normalize()
+
+ def fromstring(self, data):
+ import string, regsub
+ new = []
+ for part in regsub.split(data, self.sep):
+ list = []
+ for subp in regsub.split(part, self.rng):
+ s = string.strip(subp)
+ list.append(string.atoi(s))
+ if len(list) == 1:
+ new.append((list[0], list[0]))
+ elif len(list) == 2 and list[0] <= list[1]:
+ new.append((list[0], list[1]))
+ else:
+ raise ValueError, 'bad data passed to IntSet'
+ self.pairs = self.pairs + new
+ self.normalize()
+
+
+# Subroutines to read/write entries in .mh_profile and .mh_sequences
+
+def pickline(file, key, casefold = 1):
+ try:
+ f = open(file, 'r')
+ except IOError:
+ return None
+ pat = key + ':'
+ if casefold:
+ prog = regex.compile(pat, regex.casefold)
+ else:
+ prog = regex.compile(pat)
+ while 1:
+ line = f.readline()
+ if not line: break
+ if prog.match(line) >= 0:
+ text = line[len(key)+1:]
+ while 1:
+ line = f.readline()
+ if not line or \
+ line[0] not in string.whitespace:
+ break
+ text = text + line
+ return string.strip(text)
+ return None
+
+def updateline(file, key, value, casefold = 1):
+ try:
+ f = open(file, 'r')
+ lines = f.readlines()
+ f.close()
+ except IOError:
+ lines = []
+ pat = key + ':\(.*\)\n'
+ if casefold:
+ prog = regex.compile(pat, regex.casefold)
+ else:
+ prog = regex.compile(pat)
+ if value is None:
+ newline = None
+ else:
+ newline = '%s: %s\n' % (key, value)
+ for i in range(len(lines)):
+ line = lines[i]
+ if prog.match(line) == len(line):
+ if newline is None:
+ del lines[i]
+ else:
+ lines[i] = newline
+ break
+ else:
+ if newline is not None:
+ lines.append(newline)
+ tempfile = file + "~"
+ f = open(tempfile, 'w')
+ for line in lines:
+ f.write(line)
+ f.close()
+ os.rename(tempfile, file)
+
+
+# Test program
+
+def test():
+ global mh, f
+ os.system('rm -rf $HOME/Mail/@test')
+ mh = MH()
+ def do(s): print s; print eval(s)
+ do('mh.listfolders()')
+ do('mh.listallfolders()')
+ testfolders = ['@test', '@test/test1', '@test/test2',
+ '@test/test1/test11', '@test/test1/test12',
+ '@test/test1/test11/test111']
+ for t in testfolders: do('mh.makefolder(%s)' % `t`)
+ do('mh.listsubfolders(\'@test\')')
+ do('mh.listallsubfolders(\'@test\')')
+ f = mh.openfolder('@test')
+ do('f.listsubfolders()')
+ do('f.listallsubfolders()')
+ do('f.getsequences()')
+ seqs = f.getsequences()
+ seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
+ print seqs
+ f.putsequences(seqs)
+ do('f.getsequences()')
+ testfolders.reverse()
+ for t in testfolders: do('mh.deletefolder(%s)' % `t`)
+ do('mh.getcontext()')
+ context = mh.getcontext()
+ f = mh.openfolder(context)
+ do('f.listmessages()')
+ do('f.getcurrent()')
+
+
+if __name__ == '__main__':
+ test()
diff --git a/slime-0.11/slime b/slime-0.11/slime
new file mode 100644
index 0000000..d699796
--- /dev/null
+++ b/slime-0.11/slime
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+PYTHONPATH="@slimedir@:$PYTHONPATH"
+export PYTHONPATH
+python @slimedir@/ui.py
diff --git a/slime-0.11/slime.html b/slime-0.11/slime.html
new file mode 100644
index 0000000..575ed0d
--- /dev/null
+++ b/slime-0.11/slime.html
@@ -0,0 +1,112 @@
+<html>
+<head>
+<title>Slime - the Stupid little mailer</title>
+</head>
+<body>
+
+<h1>Slime - the Stupid little mailer</h1>
+
+<p>See the <a href="README">README</a> for more info, and <a
+href="Changes">Changes</a>, if you've seen any previous version.
+The <a href="slime-manual.html/index.html">manual</a> is not
+yet written.
+
+<p>Screenshots: <a href="slime-main.jpg">main window</a> (jpeg, 25 kB),
+<a href="slime-msg.jpg">message window</a> (jpeg, 25 kB). These are
+of version 0.2, or something.
+
+<p>You need the <a href="slime.tar.gz">Slime source package</a>,
+my <a href="../programs/pyliw-0.3.tar.gz">pyliw package (version 0.3)</a>,
+Mitch Chapman's <a href="uitools.tar.gz">UITools</a>, and
+<a href="http://www.python.org/">Python</a>. <strong>Note</strong>
+that version 0.9 of Slime needs version 0.3 of pyliw (older versions
+had a bug, now fixed).
+
+
+<h2>A small collection of links</h2>
+
+<p>Message formats:
+<ul>
+<li><a href="http://www.c2.org/~raph/pgpmime.html">PGP/MIME homepage</a>
+<li><a href="http://www.c2.org/~raph/impl.html">PGP/MIME implementation notes</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2015.txt">RFC 2015 (PGP/MIME)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1847.txt">RFC 1847 (Security
+ multiparts for MIME)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc822.txt">RFC 822 (Message
+ format for mail)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1036.txt">RFC 1036 (Message
+ format for news)</a>
+<li><a href="ftp://ftp.zoo.toronto.edu/pub/news.txt.Z">Son-of-1036</a> by
+ Henry Spencer
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1892 (Bounces)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1893 (Enhanced
+ Mail System Status Codes)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1892.txt">RFC 1894 (Extensible
+ Message Format for Delivery Status Notifications)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2045.txt">RFC 2045 (MIME I:
+ Message Bodies)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2046.txt">RFC 2046 (MIME II:
+ Media Types)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2047.txt">RFC 2047 (MIME III:
+ Header Extensions for Non-ASCII Text)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc2048.txt">RFC 2048 (MIME IV:
+ Registration Procedures)</a>
+<li><a href="http://www.cs.ruu.nl/wais/html/na-dir/mail/mime-faq/.html">MIME
+ FAQ (in HTML)</a>
+</ul>
+
+<p>Protocol related stuff:
+<ul>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc821.txt">RFC 821 (SMTP)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1939.txt">RFC 1939 (POP3)</a>
+<li><a href="http://www.imap.org">IMAP</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc977.txt">RFC (NNTP)</a>
+<li><a href="http://ftp.funet.fi/pub/doc/rfc/rfc1957.txt">RFC 1957 (Observations
+ on POP implementations)</a>
+</ul>
+
+<p>Mailbox formats
+<ul>
+<li><a href="http://www.qmail.org/qmail-manual-html/man5/mbox.html">Normal Unix mbox</a>
+<li><a href="">(?)MH folders</a>
+<li><a href="http://www.qmail.org/qmail-manual-html/man5/maildir.html">Maildir</a>
+</ul>
+
+<p>Other mailers and mail-related software:
+<ul>
+<li><a href="http://www.washington.edu/pine/tech-notes/">Pine technical
+ notes</a>
+<li><a href="http://www.ifi.uio.no/~larsi/notes/notes.html">GNUS's notes
+ on news</a>
+<li><a href="http://www.cs.hmc.edu/~me/mutt/index.html">Mutt</a>
+<li><a href="http://www.sendmail.org/">Sendmail</a>
+<li><a href="http://www.qmail.org/">Qmail</a>
+<li><a href="">(?)Exim</a>
+<li><a href="">(?)Smail</a>
+<li><a href="">(?)Procmail</a>
+<li><a href="http://andrew2.andrew.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>
+<li><a href=""></a>
+</ul>
+
+<p>Other stuff
+<ul>
+<li><a href="http://ds.internic.net/internet-drafts/draft-dusse-smime-msg-02.txt">S/MIME</a>
+<li><a href="http://www.c2.org/~raph/comparison.html">Comparison between e-mail encryption protocols</a>
+<li><a href="">Heikki Kantola</a>
+<li><a href="http://http.bsd.uchicago.edu/%7Et-pierce/news/">Tim Pierce's
+ news stuff</a>
+<li>Bill Wohler's
+ <a href="http://www.worldtalk.com/html/msg_resources/email_ref.html">Email
+ References</a>
+<li>Project Andrew
+ <a href="http://andrew2.andrew.cmu.edu/cyrus/email/email.html">Email
+ Web Resources</a>
+<li><a href="http://www2.thecia.net/users/rnewman/Good_Netkeeping_Seal">Good
+ Net-Keeping Seal of Approval</a>
+<li><a href="http://www.imc.org/workshop/mail-archive/0364.html">S/MIME insecurity</a>
+<li><a href="http://www.imc.org">Internet Mail Consortium</a>
+<li><a href="http://developer.netscape.com/software/index.html">netscape LDAP</a>
+</ul>
+
+</body>
+</html>
diff --git a/slime-0.11/slime_abstract.py b/slime-0.11/slime_abstract.py
new file mode 100644
index 0000000..4de3b82
--- /dev/null
+++ b/slime-0.11/slime_abstract.py
@@ -0,0 +1,541 @@
+"""Abstract class definition for mail folders and messages.
+
+This module defines two classes, SlimeFolder and SlimeMessage, which
+define an interface to mail folders and messages, respectively.
+Classes derived from SlimeFolder and SlimeMessage give access to
+different kinds of folders, e.g., slime_mh.py defines SlimeFolder_MH
+and SlimeMessage_MH for MH folders.
+
+Other parts of Slime use the interface defined by this module
+to access folders and messags, without having to care about the
+details of each folder type.
+
+The interface is intended to become generic enough for any kind of
+folder, including all types of local files, and remote ones as
+defined by POP and IMAP. It will certainly require refinement.
+
+Lars Wirzenius <liw@iki.fi>"""
+
+
+import string, time, regsub
+import StringIO
+
+
+#
+# Exceptions that may be raised by SlimeFolder and SlimeMessage subclasses.
+#
+UnimplementedError = "UnimplementedError"
+Error = "Error"
+
+
+def notdone():
+ """Raise hell, because subclass didn't implement all methods."""
+ raise UnimplementedError, "Subclass didn't implement all methods."
+
+class SlimeFolder:
+ """Abstract interface to a mail folder."""
+
+ def __init__(self):
+ self._is_open = 0
+ self._name = None
+ self._messages = []
+ self._last_new_messages = []
+ self._last_deleted_messages = []
+ self._subfolders = []
+ self._last_new_folders = []
+ self._last_deleted_folders = []
+ self._threads = []
+ self._dirty = 0
+
+ def __repr__(self):
+ """Return textual name of subfolder."""
+ return self._name
+
+ def get_nice_name(self):
+ """Return a user-friendly representation of the name."""
+ x = string.split(self._name, "/")
+ return x[-1]
+
+ def is_clean(self):
+ """Is this folder dirty?"""
+ return not self._dirty
+
+ def make_dirty(self):
+ """Mark the folder as dirty; commit needs to be done."""
+ self._dirty = 1
+
+ def make_clean(self):
+ """Mark the folder as clean; commit doesn't need to done."""
+ self._dirty = 0
+
+ def open(self):
+ """Open the folder. Operations below require an open folder."""
+ notdone()
+
+ def close(self):
+ """Close the folder.
+
+ This should call msg.uncache_headers for every message."""
+ notdone()
+
+ def is_open(self):
+ """Is this folder open?"""
+ return self._is_open
+
+ def _assert_open(self):
+ """Make sure this folder is open."""
+ if not self._is_open:
+ raise Error, "Using closed folder " + repr(self)
+
+ def rescan_subfolders(self):
+ """Check for changes to subfolders.
+
+ This method sets self._subfolders."""
+ notdone()
+
+ def rescan_messages(self):
+ """Check for changes to messages in folder.
+
+ This method sets self._messages."""
+ notdone()
+
+ def list_all_messages(self):
+ """Return list of SlimeMessages for all messages here."""
+ self._assert_open()
+ return self._messages
+
+ def add_message(self, file):
+ """Add message in file to folder, return new SlimeMessage.
+
+ This will change the folder immediately (i.e., doesn't
+ wait until commit)."""
+ notdone()
+
+ def move_message_here(self, msg):
+ """Move a message from another folder to this folder.
+
+ This will change the target folder immediately (i.e., doesn't
+ wait until commit), but the message won't be deleted from
+ the other folder until commit."""
+ self._assert_open()
+ newmsg = self.copy_message_here(msg)
+ msg.delete_self()
+ return newmsg
+
+ def copy_message_here(self, msg):
+ """Copy a message from another folder to this folder.
+
+ This will change the folder immediately (i.e., doesn't
+ wait until commit)."""
+ self._assert_open()
+ txt = msg.getfulltext()
+ return self.add_message(StringIO.StringIO(txt))
+
+ def list_all_threads(self):
+ """Return the thread forest of a folder."""
+ self._assert_open()
+ if not self._threads:
+ self.rethread_folder()
+ return self._threads
+
+ def rethread_folder(self):
+ """Build new thread data structure."""
+ self._assert_open()
+ children = {}
+ ids = {}
+ roots = []
+ for msg in self._messages:
+ parent = msg.get_parent_id()
+ date = msg.get_linear_time()
+ subj = string.lower(msg["subject"])
+ subj = regsub.gsub("^re: *", "", subj)
+ if parent:
+ if children.has_key(parent):
+ children[parent].append(date, subj, msg)
+ else:
+ children[parent] = [(date, subj, msg)]
+ else:
+ roots.append(date, subj, msg)
+ id = msg["message-id"]
+ if id:
+ ids[id] = msg
+ for id in children.keys():
+ children[id].sort()
+ if not ids.has_key(id):
+ for date, subj, msg in children[id]:
+ roots.append(date, subj, msg)
+
+ subjects = {}
+ for date, subj, msg in roots:
+ subjects[subj] = (date, subj, msg)
+ aux_roots = []
+ for subj in subjects.keys():
+ aux_roots.append(subjects[subj])
+ aux_roots.sort()
+ real_roots = []
+ for date, subj, msg in aux_roots:
+ list = []
+ for date2, subj2, msg2 in roots:
+ if subj == subj2:
+ list.append(date2, msg2)
+ list.sort()
+ real_roots = real_roots + list
+
+ self._threads = []
+ for date, msg in real_roots:
+ self.append_thread(0, msg, children)
+
+ def append_thread(self, level, msg, children):
+ """Append msg to self._threads, and then all its children."""
+ self._threads.append(level, msg)
+ id = msg["message-id"]
+ if children.has_key(id):
+ level = level + 1
+ for date, subj, child in children[id]:
+ self.append_thread(level, child, children)
+
+ def get_msg_level(self, msg):
+ """Return indentation level of message in thread display."""
+ self._assert_open()
+ for i, m in self._threads:
+ if m == msg:
+ return i
+ return 0
+
+ def list_all_subfolders(self):
+ """Return list of SlimeFolder objects for subfolders."""
+ self._assert_open()
+ return self._subfolders
+
+ def add_subfolder(self, name):
+ """Add or create a new subfolder named `name'."""
+ notdone()
+
+ def move_subfolder_here(self, subfolder_object):
+ """Move the subfolder to this folder."""
+ # xxx placeholder
+ notdone()
+
+ def delete_self(self):
+ """Remove this folder."""
+ # xxx place holder
+ notdone()
+
+ def rename_self(self, new_name):
+ """Rename this folder to have a new name."""
+ # xxx place holder
+ notdone()
+
+ def commit_changes(self):
+ """Commit all changes to folder and its messages."""
+ notdone()
+
+_interned_strings = {}
+def _intern_string(str):
+ """Make sure there is only one instance of a given string."""
+ try:
+ str = _interned_strings[str]
+ except KeyError:
+ _interned_strings[str] = str
+ return str
+
+interesting_headers = ["from", "subject", "to", "cc", "date", "status",
+ "in-reply-to", "message-id", "references"]
+
+class SlimeMessage:
+ """Abstract interface to a single e-mail message.
+
+ Status: Every message has a status, which is a string consisting
+ of single-character flags (graphical flags only):
+
+ N = new message
+ D = message should be deleted at commit time
+ (still searching for good info on the chars)
+
+ """
+
+ # Note that some operations may require changes to the folder
+ # the message resides in. Changes are commited only after the
+ # folder is commited.
+
+
+ def __init__(self, folder):
+ """Initialize the message.
+
+ Derived classes must call this."""
+ self._folder = folder
+ self._msg = None
+ self._status = ""
+ self._cached_headers = {}
+ self._cached_date = None
+ self._open_count = 0
+
+ def _open(self):
+ """Open the message. Done automatically in background."""
+ notdone()
+
+ def _close(self):
+ """Close the message. Done automatically in background."""
+ notdone()
+
+ def _assert_open(self):
+ """Assert that the folder this message is in is open."""
+ self._folder._assert_open()
+
+ def uncache_headers(self):
+ """Forget about cached headers."""
+ self._assert_open()
+ self._cached_headers = {}
+
+ def get_status(self):
+ """Return status of this message (read/unread, etc)."""
+ self._assert_open()
+ return self._status
+
+ def _clean_status(self, status):
+ cleaned = ""
+ for c in "ND":
+ if c in status:
+ cleaned = cleaned + c
+ return cleaned
+
+ def set_status(self, new_status):
+ """Change the status of this message, don't mark folder dirty."""
+ self._assert_open()
+ self._status = self._clean_status(new_status)
+
+ def has_status(self, statuschar):
+ """Does the message status have statuschar?"""
+ self._assert_open()
+ return string.find(self._status, statuschar) >= 0
+
+ def give_status(self, statuschar):
+ """Add statuschar to status of message."""
+ self._assert_open()
+ cleaned = self._clean_status(statuschar)
+ if statuschar and string.find(self._status, cleaned) == -1:
+ self._folder.make_dirty()
+ self._status = self._status + cleaned
+
+ def remove_status(self, statuschar):
+ """Remove statuschar from status of message."""
+ self._assert_open()
+ if statuschar:
+ i = string.find(self._status, statuschar)
+ if i >= 0:
+ self._folder.make_dirty()
+ self._status = self._status[:i] + \
+ self._status[i+1:]
+
+ def change_text(self, file):
+ """Change the contents of the message to that in file.
+
+ Note that not all folder formats support this
+ functionality easily. It is needed for the draft
+ folder only, and that one is always an MH folder,
+ so it gives an error for other folder types."""
+ raise Error, "change_text is not supported for this folder"
+
+ def delete_self(self):
+ """Delete this message from the folder it is in."""
+ self._assert_open()
+ self.give_status("D")
+ self._folder.make_dirty()
+
+ def undelete_self(self):
+ """Undelete this message from the folder it is in."""
+ self._assert_open()
+ self.remove_status("D")
+
+ def _find_ids(self, str):
+ """Return list of Message-ID's in a string."""
+ str = string.join(string.split(str, "\n"), " ")
+ str = string.join(string.split(str, "\t"), " ")
+ str = regsub.sub("^[^<]*", "", str)
+ str = regsub.gsub(">[^<]*<", "> <", str)
+ str = regsub.sub(">[^<]*$", ">", str)
+ str = regsub.sub(" *", " ", str)
+ return string.split(str)
+
+ def get_parent_id(self):
+ """Return the Message-ID of the message this is a reply to."""
+ self._assert_open()
+ irt = self["in-reply-to"]
+ if irt:
+ ids = self._find_ids(irt)
+ if ids:
+ return ids[0]
+ refs = self["references"]
+ if refs:
+ ids = self._find_ids(refs)
+ if ids:
+ return ids[-1]
+ return ""
+
+ def get_linear_time(self):
+ """Return the Date: header converted to seconds since epoch."""
+ self._assert_open()
+ date = self.getdate("date")
+ if date is None:
+ return 0
+ return time.mktime(date)
+
+ def getfulltext(self):
+ """Return full text of message."""
+ return self.getheadertext() + "\n" + self.getbodytext(0)
+
+ #
+ # The following are the interface provided together by
+ # rfc822.Message, mimetools.Message, and mhlib.Message.
+ #
+
+ def getallmatchingheaders(self, name):
+ self._open()
+ x = self._msg.getallmatchingheaders(name)
+ self._close()
+ return x
+ def getfirstmatchingheader(self, name):
+ self._open()
+ x = self._msg.getfirstmatchingheader(name)
+ self._close()
+ return x
+ def getrawheader(self, name):
+ self._open()
+ x = self._msg.getrawheader(name)
+ self._close()
+ return x
+ def getheader(self, name):
+ self._open()
+ x = self._msg.getheader(name)
+ self._close()
+ return x
+ def getaddr(self, name):
+ self._open()
+ x = self._msg.getaddr(name)
+ self._close()
+ return x
+ def getaddrlist(self, name):
+ self._open()
+ x = self._msg.getaddrlist(name)
+ self._close()
+ return x
+ def getdate(self, name):
+ if string.lower(name) == "date":
+ if not self._cached_date:
+ self._open()
+ self._cache_headers()
+ self._close()
+ return self._cached_date
+ else:
+ self._open()
+ date = self._msg.getdate(name)
+ self._close()
+ return date
+ def __len__(self):
+ self._open()
+ x = self._msg.__len__()
+ self._close()
+ return x
+ def _cache_headers(self):
+ self._assert_open()
+ for h in interesting_headers:
+ try:
+ x = self._msg.__getitem__(h)
+ except KeyError:
+ x = ""
+ x = _intern_string(x)
+ self._cached_headers[h] = x
+ self._cached_date = self._msg.getdate("date")
+ if not self._cached_date:
+ self._cached_date = (1970,1,1,0,0,0,3,1,0)
+ def __getitem__(self, name):
+ # Note: we assume it is benign to return "" for missing header.
+ name = string.lower(name)
+ if self._cached_headers.has_key(name):
+ return self._cached_headers[name]
+ self._open()
+ try:
+ x = self._msg.__getitem__(name)
+ except KeyError:
+ x = ""
+ x = _intern_string(x)
+ self._cached_headers[name] = x
+ self._close()
+ return x
+ def has_key(self, name):
+ self._open()
+ x = self._msg.has_key(name)
+ self._close()
+ return x
+ def keys(self):
+ self._open()
+ x = self._msg.keys()
+ self._close()
+ return x
+ def values(self):
+ self._open()
+ x = self._msg.values()
+ self._close()
+ return x
+ def items(self):
+ self._open()
+ x = self._msg.items()
+ self._close()
+ return x
+
+ def getplist(self):
+ self._open()
+ x = self._msg.getplist()
+ self._close()
+ return x
+ def getparam(self, name):
+ self._open()
+ x = self._msg.getparam(name)
+ self._close()
+ return x
+ def getparamnames(self):
+ self._open()
+ x = self._msg.getparamnames()
+ self._close()
+ return x
+ def getencoding(self):
+ self._open()
+ x = self._msg.getencoding()
+ self._close()
+ return x
+ def gettype(self):
+ self._open()
+ x = self._msg.gettype()
+ self._close()
+ return x
+ def getmaintype(self):
+ self._open()
+ x = self._msg.getmaintype()
+ self._close()
+ return x
+ def getsubtype(self):
+ self._open()
+ x = self._msg.getsubtype()
+ self._close()
+ return x
+
+ def getheadertext(self, pred = None):
+ self._open()
+ x = self._msg.getheadertext(pred)
+ self._close()
+ return x
+ def getbodytext(self, decode = 1):
+ self._open()
+ x = self._msg.getbodytext(decode)
+ self._close()
+ return x
+ def getbodyparts(self):
+ self._open()
+ x = self._msg.getbodyparts()
+ self._close()
+ return x
+ def getbody(self):
+ self._open()
+ x = self._msg.getbody()
+ self._close()
+ return x
diff --git a/slime-0.11/slime_draft.py b/slime-0.11/slime_draft.py
new file mode 100644
index 0000000..dfc1053
--- /dev/null
+++ b/slime-0.11/slime_draft.py
@@ -0,0 +1,52 @@
+"""Handle the draft folder."""
+
+import os, slime_mh
+import mhlib2
+mhlib = mhlib2
+
+SLIME_DIR = "~/.slime"
+DRAFT_FOLDER = "~/.slime/internal_folders"
+DRAFT_PROFILE = "~/.slime/.internal_mh_profile"
+slime_dir = None
+draft_path = None
+draft_profile = None
+
+draft_mh = None
+draft_folder = None
+
+def make_draft_folder():
+ global draft_mh, draft_folder, draft_path, draft_profile, slime_dir
+
+ if not draft_path:
+ slime_dir = os.path.expanduser(SLIME_DIR)
+ draft_path = os.path.expanduser(DRAFT_FOLDER)
+ draft_profile = os.path.expanduser(DRAFT_PROFILE)
+ if not os.path.isdir(draft_path):
+ os.mkdir(slime_dir)
+ os.mkdir(draft_path)
+ os.mkdir(os.path.join(draft_path, "drafts"))
+ f = open(os.path.join(draft_path, ".folders"), "w")
+ f.write("drafts\n")
+ f.close()
+ if not os.path.isfile(draft_profile):
+ f = open(draft_profile, "w")
+ f.close()
+
+ draft_mh = mhlib.MH(draft_path, draft_profile)
+ draft_folder = SlimeFolder_Draft('drafts', draft_mh)
+ return draft_folder
+
+class SlimeFolder_Draft(slime_mh.SlimeFolder_MH):
+ def close(self):
+ pass
+
+
+def main():
+ f = draft_top_folder()
+ f.open()
+ f.rescan_subfolders()
+ print f.list_all_subfolders()
+ f.close()
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/slime_folderinfo.py b/slime-0.11/slime_folderinfo.py
new file mode 100644
index 0000000..041b2f1
--- /dev/null
+++ b/slime-0.11/slime_folderinfo.py
@@ -0,0 +1,77 @@
+import helpers, string, os
+
+_folderinfo = {}
+
+def load_folderinfo(filename):
+ global _folderinfo
+ try:
+ f = open(filename, "r")
+ except IOError:
+ _folderinfo = {}
+ return
+ while 1:
+ line = f.readline()
+ if not line:
+ break
+ foldername = string.split(line, "\n")[0]
+ dict = {}
+ while 1:
+ line = f.readline()
+ if not line or line == "\n":
+ break
+ parts = string.split(line, " ", 1)
+ field = parts[0]
+ value = string.split(parts[1], "\n")[0]
+ dict[field] = value
+ _folderinfo[foldername] = dict
+ f.close()
+
+def save_folderinfo(filename):
+ text = []
+ for foldername in _folderinfo.keys():
+ text.append("%s\n" % foldername)
+ dict = _folderinfo[foldername]
+ for field in dict.keys():
+ text.append("%s %s\n" % (field, dict[field]))
+ text.append("\n")
+ text = string.join(text, "")
+ helpers.write_text_to_named_file(filename + ".new", text)
+ try:
+ os.remove(filename)
+ except os.error:
+ pass
+ os.rename(filename + ".new", filename)
+
+def get_folderinfo(foldername, field):
+ try:
+ return _folderinfo[foldername][field]
+ except KeyError:
+ return None
+
+def set_folderinfo(foldername, field, value):
+ try:
+ dict = _folderinfo[foldername]
+ except KeyError:
+ dict = {}
+ _folderinfo[foldername] = dict
+ dict[field] = value
+
+def get_folders_with_info(field, value):
+ list = []
+ for foldername in _folderinfo.keys():
+ try:
+ if _folderinfo[foldername][field] == value:
+ list.append(foldername)
+ except KeyError:
+ pass
+ return list
+
+def main():
+ load_folderinfo("folderinfo.test")
+ print get_folderinfo("/home/liw/Mail/inbox", "inbox")
+ set_folderinfo("/home/liw/Mail/inbox", "inbox", "no")
+ print get_folderinfo("/home/liw/Mail/inbox", "inbox")
+ save_folderinfo("folderinfo.test")
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/slime_mh.py b/slime-0.11/slime_mh.py
new file mode 100644
index 0000000..a4571d6
--- /dev/null
+++ b/slime-0.11/slime_mh.py
@@ -0,0 +1,245 @@
+"""Implement interface to MH folders."""
+
+from slime_abstract import *
+import os
+
+import mhlib2
+mhlib = mhlib2 # kludge until Python accepts my modified mhlib.py
+import StringIO2
+StringIO = StringIO2
+
+default_mh = None
+
+def mh_top_folder():
+ """Return SlimeFolder with top level MH folders."""
+ global default_mh
+ if not default_mh:
+ default_mh = mhlib.MH()
+ mhroot = SlimeFolder_MHroot(default_mh)
+ mhroot.open()
+ mhroot.rescan_subfolders()
+ if mhroot.list_all_subfolders():
+ mhroot.close()
+ return mhroot
+ mhroot.close()
+ return None
+
+class SlimeFolder_MHroot(SlimeFolder):
+ """Root of all MH folders."""
+
+ def __init__(self, mh):
+ SlimeFolder.__init__(self)
+ self._mh = mh
+ self._name = mh.getpath()
+ self._subfolders = []
+
+ def open(self):
+ self._is_open = 1
+ self.rescan_subfolders()
+
+ def close(self):
+ self._is_open = 0
+
+ def commit_changes(self):
+ self._assert_open()
+
+ def rescan_messages(self):
+ pass
+
+ def rescan_subfolders(self):
+ self._assert_open()
+ list = []
+ for name in self._mh.listfolders():
+ if not os.path.isabs(name):
+ name = os.path.join(self._mh.getpath(), name)
+ new = None
+ for subfolder in self._subfolders:
+ if name == repr(subfolder):
+ new = subfolder
+ break
+ if new is None:
+ new = SlimeFolder_MH(name, self._mh)
+ list.append(new)
+ self._subfolders = list
+
+class SlimeFolder_MH(SlimeFolder):
+ """Abstract interface to an MH mail folder."""
+
+ def __init__(self, name, mh):
+ SlimeFolder.__init__(self)
+ self.mh = mh
+ self._subfolders = []
+ self._name = os.path.join(mh.getpath(), name)
+ self.mh_folder = self.mh.openfolder(self._name)
+ self._last_message_scan = 0
+
+ def open(self):
+ self._is_open = 1
+ self._old_numbers = []
+
+ def close(self):
+ """Close the folder."""
+ for msg in self._messages:
+ msg.uncache_headers()
+ self._is_open = 0
+
+ def rescan_messages(self):
+ self._assert_open()
+
+ dirname = self.mh_folder.getfullname()
+ dirmtime = os.stat(dirname)[8]
+ if dirmtime == self._last_message_scan:
+ return
+ self._last_message_scan = dirmtime
+
+ seq = self.mh_folder.getsequences()
+ if seq.has_key("unseen"):
+ unseen = seq["unseen"]
+ else:
+ unseen = []
+
+ numbers = self.mh_folder.listmessages()
+ list = []
+ for n in numbers:
+ if n in self._old_numbers:
+ for msg in self._messages:
+ if msg._id == n:
+ list.append(msg)
+ break
+ else:
+ msg = SlimeMessage_MH(self, n)
+ if n in unseen:
+ msg.set_status("N")
+ list.append(msg)
+ self._messages = list
+ self._old_numbers = numbers
+ self._threads = []
+
+ def rescan_subfolders(self):
+ oldlist = []
+ for old in self._subfolders:
+ oldlist.append(repr(old))
+ list = []
+ for name in self.mh_folder.listsubfolders():
+ fullname = os.path.join(self.mh.getpath(), name)
+ try:
+ i = oldlist.index(fullname)
+ new = self._subfolders[i]
+ except ValueError:
+ new = SlimeFolder_MH(name, self.mh)
+ list.append(new)
+ self._subfolders = list
+
+ def add_message(self, file):
+ self._assert_open()
+ f = self.mh_folder
+ n = f.getlast() + 1
+ f.createmessage(n, file)
+ msg = SlimeMessage_MH(self, n)
+ self._messages.append(msg)
+ self._old_numbers.append(n)
+ return msg
+
+ def commit_changes(self):
+ if self._dirty:
+ self._assert_open()
+ unseen = []
+ list = []
+ for msg in self._messages:
+ if msg.has_status("D"):
+ list.append(msg._id)
+ elif msg.has_status("N"):
+ unseen.append(msg._id)
+ self.mh_folder.removemessages(list)
+
+ seq = self.mh_folder.getsequences()
+ if seq.has_key("unseen"):
+ unseen.sort()
+ seq["unseen"] = unseen
+ self.mh_folder.putsequences(seq)
+
+ self._dirty = 0
+
+ self.rescan_messages()
+
+class SlimeMessage_MH(SlimeMessage):
+ """A SlimeMessage interface to mhlib.Message."""
+
+ def __init__(self, folder, id):
+ SlimeMessage.__init__(self, folder)
+ self._msg = None
+ self._id = id
+
+ def _open(self):
+ self._folder._assert_open()
+ self._open_count = self._open_count + 1
+ if not self._msg:
+ self._msg = mhlib.Message(self._folder.mh_folder,
+ self._id)
+ # make sure stuff is cached
+ if not self._cached_date:
+ self._cache_headers()
+
+ def _close(self):
+ self._folder._assert_open()
+ self._open_count = self._open_count - 1
+ if self._open_count == 0:
+ self._msg = None
+
+ def change_text(self, file):
+ self._folder._assert_open()
+ if self._open_count != 0:
+ raise Error, "change_text needs closed message"
+ filename = self._folder.mh_folder.getmessagefilename(self._id)
+ newname = filename + '.new'
+ f = open(newname, "w")
+ while 1:
+ buf = file.read(16*1024)
+ if not buf:
+ break
+ f.write(buf)
+ f.close()
+ os.rename(newname, filename)
+ self.uncache_headers()
+
+def main():
+ root = mh_top_folder()
+ if not root:
+ print "no MH root"
+ else:
+# root.open()
+# root.rescan_subfolders()
+# print root.list_all_subfolders()
+# root.close()
+ foo(root)
+
+def foo(folder):
+ folder.open()
+ folder.rescan_subfolders()
+ folder.close()
+ folder.open()
+ folder.rescan_subfolders()
+ print folder
+ for sub in folder.list_all_subfolders():
+ foo(sub)
+ folder.close()
+
+def profile_main():
+ import time
+ f = mh_top_folder()
+ f.open()
+ f.rescan_subfolders()
+ for sub in f.list_all_subfolders():
+ if sub.get_nice_name() == "cola-incoming":
+ sub.open()
+ print "computing threads"
+ start = time.time()
+ sub.rescan_messages()
+ sub.list_all_threads()
+ end = time.time()
+ print "threads computed", end - start
+ sub.close()
+ f.close()
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/slime_pgpmime.py b/slime-0.11/slime_pgpmime.py
new file mode 100644
index 0000000..e02d06d
--- /dev/null
+++ b/slime-0.11/slime_pgpmime.py
@@ -0,0 +1,168 @@
+"""PGP/MIME interface for Slime.
+
+Parts of this module is based on the algorithms in pgpExec.tcl in
+Exmh 2.0gamma.
+
+"""
+
+from slime_abstract import *
+from slime_mh import *
+from slime_unix import *
+from ui_config import config
+import slime_send, helpers
+
+import tempfile, regex, mimetools, os, FCNTL
+
+
+# Exception names for results of PGP stuff
+SecretMissing = "slime_pgp.SecretMissing"
+PublicMissing = "slime_pgp.PublicMissing"
+GoodUntrustedSignature = "slime_pgp.GoodUntrustedSignature"
+BadUntrustedSignature = "slime_pgp.BadUntrustedSignature"
+BadTrustedSignature = "slime_pgp.BadTrustedSignature"
+OtherResult = "slime_pgp.OtherResult"
+
+# Regular expressions for interpreting the PGP results
+secret_missing_pat = \
+ regex.compile("This.*do not have the secret key.*file", regex.casefold)
+public_missing_pat = \
+ regex.compile("Can't.*can't check signature integrity", regex.casefold)
+good_signature_pat = \
+ regex.compile("Good signature", regex.casefold)
+confidence_pat = \
+ regex.compile("WARNING:.*confidence", regex.casefold)
+doesnt_match_pat = \
+ regex.compile("WARNING:.*doesn't match", regex.casefold)
+
+def interpret_pgp(exit_code, stdout, stderr):
+ """Interpret the output of PGP, raise exception for errors."""
+ tuple = (exit_code, stdout, stderr)
+
+ if secret_missing_pat.search(stdout) >= 0:
+ raise SecretMissing, tuple
+ elif public_missing_pat.search(stdout) >= 0:
+ raise PublicMissing, tuple
+ elif good_signature_pat.search(stdout) >= 0:
+ if confidence_pat.search(stdout) >= 0:
+ raise GoodUntrustedSignature, tuple
+ else:
+ return # Yay! It's OK! Way to go, man!
+ elif doesnt_match_pat.search(stdout) >= 0:
+ if confidence_pat.search(stdout) >= 0:
+ raise BadUntrustedSignature, tuple
+ else:
+ raise BadTrustedSignature, tuple
+ else:
+ raise OtherResult, tuple
+
+def write_to_named_file(filename, msg):
+ helpers.write_text_to_named_file(filename,
+ msg.getheadertext() + "\n" + msg.getbodytext()[:-1])
+ # XXX note that the [:-1] part is a kludge -- exmh
+ # creates an extra newline or something
+
+def check_signature(msg):
+ """Check signature of a PGP/MIME message, raise exception for errors."""
+ if msg.gettype() != "multipart/signed" or \
+ not "protocol" in msg.getparamnames() or \
+ msg.getparam("protocol") != "application/pgp-signature" or \
+ not "micalg" in msg.getparamnames() or \
+ msg.getparam("micalg") != "pgp-md5":
+ return None
+
+ parts = msg.getbodyparts()
+ msgtxt = tempfile.mktemp()
+ msgsig = tempfile.mktemp()
+
+ write_to_named_file(msgtxt, parts[0])
+ write_to_named_file(msgsig, parts[1])
+
+ exit_code, stdout, stderr = \
+ helpers.run_pgp(config["pgp-path"], "", "", [], [msgsig, msgtxt])
+ os.remove(msgtxt)
+ os.remove(msgsig)
+ interpret_pgp(exit_code, stdout, stderr)
+
+def filter_content_headers(headername):
+ headername = string.lower(headername)
+ return not headername in ["content-type", "content-transfer-encoding"]
+
+def make_signature(msg, username, password):
+ """Return string with text of signed message."""
+
+ body_type = msg["content-type"]
+ body_xfer = msg["content-transfer-encoding"]
+ if not body_type or body_type == "text/plain":
+ charset = slime_send.deduce_charset(msg.getbodytext())
+ body_type = "text/plain; charset=%s" % charset
+ if charset == "us-ascii":
+ body_xfer = "7bit"
+ else:
+ body_xfer = "8bit"
+
+ msg_headers = msg.getheadertext(filter_content_headers)
+ msg_body = msg.getbodytext(decode=0)
+
+ boundary = mimetools.choose_boundary()
+ msg_type = 'multipart/signed; boundary=%s;\n' + \
+ ' micalg=pgp-md5; protocol="application/pgp-signature"'
+ msg_type = msg_type % boundary
+
+ part1_headers = "Content-Type: %s\nContent-Transfer-Encoding: %s\n" % \
+ (body_type, body_xfer)
+ part1 = part1_headers + "\n" + msg_body
+
+ exit_code, stdout, stderr = \
+ helpers.run_pgp(config["pgp-path"], username, password,
+ ["-sbatf"], [], stdin=part1)
+ if exit_code != 0:
+ raise OtherResult, (exit_code, stdout, stderr)
+
+ part2_headers = "Content-Type: application/pgp\n"
+ part2 = part2_headers + "\n" + stdout
+
+ return "%sContent-Type: %s\n\n--%s\n%s\n--%s\n%s\n--%s--\n" % \
+ (msg_headers, msg_type, boundary, part1,
+ boundary, part2, boundary)
+
+
+def test_check_signature():
+ mhroot = mh_top_folder()
+ mhroot.open()
+ mhroot.rescan_subfolders()
+ for sub in mhroot.list_all_subfolders():
+ if sub.get_nice_name() == "sent-mail":
+ sub.open()
+ sub.rescan_messages()
+ list = sub.list_all_messages()[:1]
+ if list:
+ msg = list[0]
+ result = check_signature(msg)
+ print "Check result:"
+ print result
+ sub.close()
+ mhroot.close()
+
+def test_make_signature():
+ import sys
+ mhroot = mh_top_folder()
+ mhroot.open()
+ mhroot.rescan_subfolders()
+ for sub in mhroot.list_all_subfolders():
+ if sub.get_nice_name() == "drafts":
+ sub.open()
+ sub.rescan_messages()
+ list = sub.list_all_messages()[:1]
+ if list:
+ msg = list[0]
+ print "Enter username:"
+ username = sys.stdin.readline()[:-1]
+ print "Enter password:"
+ password = sys.stdin.readline()[:-1]
+ result = make_signature(msg, username, password)
+ print result
+ sub.close()
+ mhroot.close()
+
+if __name__ == "__main__":
+ test_check_signature()
diff --git a/slime-0.11/slime_root.py b/slime-0.11/slime_root.py
new file mode 100644
index 0000000..c265801
--- /dev/null
+++ b/slime-0.11/slime_root.py
@@ -0,0 +1,77 @@
+"""Implement the root folder."""
+
+from slime_abstract import SlimeFolder, Error
+
+class SlimeFolder_Root(SlimeFolder):
+ """The root folder that lists all folder collections."""
+
+ def __init__(self):
+ SlimeFolder.__init__(self)
+ self._name = "Root folder"
+
+ def open(self):
+ self._is_open = 1
+
+ def close(self):
+ self._is_open = 0
+
+ def rescan_subfolders(f):
+ self._assert_open()
+
+ def rescan_messages(f):
+ self._assert_open()
+
+ def add_message(self, message):
+ raise Error, "Can't add messages to root folder."
+
+ def delete_message(self, id):
+ raise Error, "Can't delete messages from root folder."
+
+ def open_message(self, id):
+ raise Error, "Can't open messages in root folder."
+
+ def add_subfolder(self, object):
+ """Add a subfolder `object'.
+
+ Note that the interface is different from SlimeFolder:
+ the root folder can't know the type of the folder, so
+ it gets an object, instead of just the name."""
+ self._assert_open()
+ self._subfolders.append(object)
+
+ def delete_self(self):
+ raise Error, "Can't remove root folder."
+
+ def rename_self(self, new_name):
+ raise Error, "Can't rename root folder."
+
+ def commit_changes(self):
+ # For the root folder, everything is happens at once.
+ self._assert_open()
+
+
+def main():
+ """Test the root folder."""
+ from slime_mh import *
+ from slime_unix import *
+
+ r = SlimeFolder_Root()
+
+ r.open()
+ r.add_subfolder(SlimeFolder_Unix(os.path.expanduser("~/mail")))
+
+ for f in r.list_all_subfolders():
+ print repr(f)
+ f.open()
+ f.rescan_subfolders()
+ f.rescan_messages()
+ for m in f.list_all_threads()[:2]:
+ print m["subject"]
+ print "Subfolders:", f.list_all_subfolders()
+ f.close()
+ print
+
+ r.close()
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/slime_send.py b/slime-0.11/slime_send.py
new file mode 100644
index 0000000..3e6c3b8
--- /dev/null
+++ b/slime-0.11/slime_send.py
@@ -0,0 +1,171 @@
+import smtplib, string, regex, regsub, pwd, os, socket, rfc822, time
+import helpers, StringIO
+from ui_config import config
+
+draft_txt = """From: %s
+Reply-to: %s
+To:
+Cc:
+Subject:
+
+%s"""
+
+reply_txt = """From: %s
+Reply-to: %s
+To: %s
+Cc: %s
+Subject: %s%s
+In-Reply-To: %s
+References: %s
+
+%s:
+%s
+
+%s"""
+
+_user_address = None
+
+def deduce_user_address():
+ global _user_address
+ if config["from-address"]:
+ return config["from-address"]
+ if _user_address is None:
+ userinfo = pwd.getpwuid(os.getuid())
+ host = socket.gethostname()
+ ip = socket.gethostbyname(host)
+ fqdn = socket.gethostbyaddr(ip)[0]
+ _user_address = "%s@%s" % (userinfo[0], fqdn)
+ if userinfo[4]:
+ x = regsub.sub(",*$", "", userinfo[4])
+ _user_address = "%s <%s>" % (x, _user_address)
+ return _user_address
+
+def get_signature_text():
+ try:
+ pathname = os.path.expanduser(config["signature-file"])
+ sig = "-- \n" + helpers.read_text_from_named_file(pathname)
+ except IOError:
+ sig = ""
+ return sig
+
+def make_new_message_text():
+ return draft_txt % (deduce_user_address(), config["reply-to"],
+ get_signature_text())
+
+def remove_duplicate_addrs(recipients, used_addrs):
+ result = []
+ for recipient in string.split(recipients, ","):
+ name, addr = rfc822.parseaddr(recipient)
+ if not addr in used_addrs:
+ result.append(recipient)
+ used_addrs.append(addr)
+ return string.join(result, ","), used_addrs
+
+def make_reply_message_text(msg):
+ user_addr = deduce_user_address()
+ reply_to = config["reply-to"]
+ to_addrs = msg["to"]
+ if to_addrs:
+ to_addrs = msg["from"] + ", " + to_addrs
+ else:
+ to_addrs = msg["from"]
+ cc_addrs = msg["cc"]
+
+ user_addr_name, user_addr_addr = rfc822.parseaddr(user_addr)
+ reply_to_name, reply_to_addr = rfc822.parseaddr(reply_to)
+
+ if user_addr_addr and reply_to_addr:
+ used_addrs = [user_addr_addr, reply_to_addr]
+ elif user_addr_addr:
+ used_addrs = [user_addr_addr]
+ elif reply_to_addr:
+ used_addrs = [reply_to_addr]
+ else:
+ used_addrs = []
+ to_addrs, used_addrs = remove_duplicate_addrs(to_addrs, used_addrs)
+ cc_addrs, used_addrs = remove_duplicate_addrs(cc_addrs, used_addrs)
+
+ subject = msg["subject"]
+ if string.lower(subject[:3]) == "re:":
+ re = ""
+ else:
+ re = "Re: "
+
+ msgid = msg["message-id"]
+ refs = msg["references"] + " " + msgid
+ while len(refs) > config["max-references-length"]:
+ new_refs = regsub.sub("[^ ]* ", "", refs)
+ if len(new_refs) == refs:
+ break
+ refs = new_refs
+
+ quoted_author = msg["from"]
+ quoted = msg.getbodytext()
+ quoted = regsub.gsub("^", "> ", quoted)
+ if quoted and quoted[-1] != "\n":
+ quoted = quoted + "\n"
+
+ return reply_txt % (user_addr, reply_to, to_addrs,
+ cc_addrs, re, subject, msgid, refs,
+ quoted_author, quoted, get_signature_text())
+
+empty_trans = string.maketrans("", "")
+sevenbit_chars = None
+
+def deduce_charset(str):
+ """Deduce the character set used in a string."""
+ global sevenbit_chars
+ if not sevenbit_chars:
+ list = []
+ for code in range(128):
+ list.append(chr(code))
+ sevenbit_chars = string.join(list, "")
+ if string.translate(str, empty_trans, sevenbit_chars):
+ return "ISO-8859-1"
+ return "us-ascii"
+
+empty_header = regex.compile("^[^:][^:]*:[ \t]*$")
+date_header = regex.compile("^Date:", regex.casefold)
+
+def prepare_for_send(msg):
+ nonempty = []
+ hit = 0
+ for line in string.split(msg.getheadertext(), "\n"):
+ if line and not line[0] in string.whitespace:
+ if date_header.match(line) == -1:
+ hit = (empty_header.match(line) == -1)
+ else:
+ hit = 0
+ if line and hit:
+ nonempty.append(line)
+ nonempty.append("Date: %s" % helpers.rfc822_date())
+ nonempty.append("")
+ headers = string.join(nonempty, "\n")
+
+ msg.change_text(StringIO.StringIO(headers + "\n" + msg.getbodytext(0)))
+
+def send_msg(msg):
+ name, sender = msg.getaddr("from")
+ receivers = []
+ for header in ["to", "cc"]:
+ if msg[header]:
+ for name, addr in msg.getaddrlist(header):
+ receivers.append(addr)
+
+ smtp = smtplib.SMTP(config["smtp-server"])
+ smtp.helo()
+ smtp.mail_from(sender)
+ for receiver in receivers:
+ smtp.rcpt_to(receiver)
+ text = string.split(msg.getfulltext(), "\n")
+ smtp.data(text)
+ smtp.quit()
+
+def main():
+ print deduce_user_address()
+ print rfc822_date()
+ print deduce_charset("this is us-ascii")
+ print deduce_charset("tämä on latin1:tä")
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/slime_unix.py b/slime-0.11/slime_unix.py
new file mode 100644
index 0000000..0410aa0
--- /dev/null
+++ b/slime-0.11/slime_unix.py
@@ -0,0 +1,183 @@
+import mhlib, os, StringIO2
+StringIO = StringIO2
+from slime_abstract import *
+from mailbox import UnixMailbox, _Subfile
+
+def unix_top_folder(dirname):
+ """Return SlimeFolder with top level Unix folders."""
+ dirname = os.path.expanduser(dirname)
+ if os.path.isdir(dirname):
+ if not os.path.isfile(os.path.join(dirname, "context")):
+ return SlimeFolder_Unix(dirname)
+ return None
+
+class SlimeFolder_Unix(SlimeFolder):
+ """A SlimeFolder interface to ordinary Unix mailbox folders."""
+
+ def __init__(self, name):
+ SlimeFolder.__init__(self)
+ if not os.path.isabs(name):
+ name = os.path.join(os.getcwd(), name)
+ self._name = name
+ self._is_dir = 0
+
+ def open(self):
+ self._is_open = 1
+ if os.path.isdir(self._name):
+ self._is_dir = 1
+ else:
+ self._is_dir = 0
+ self._fp = open(self._name, 'r+')
+
+ def close(self):
+ for msg in self._messages:
+ msg.uncache_headers()
+ if not self._is_dir:
+ self._fp = None
+ self._is_open = 0
+
+ def rescan_messages(self):
+ self._assert_open()
+ if not self._is_dir:
+ new_messages = []
+ mb = UnixMailbox(self._fp)
+ while 1:
+ m = mb.next()
+ if not m:
+ break
+ mm = SlimeMessage_Unix(self, m.fp)
+ for msg in self._messages:
+ if mm._checksum == msg._checksum:
+ mm = msg
+ break
+ mm.set_status(mm["status"])
+ new_messages.append(mm)
+ self._messages = new_messages
+ self._threads = []
+
+ def rescan_subfolders(self):
+ self._assert_open()
+ if self._is_dir:
+ list = []
+ for pathname in os.listdir(self._name):
+ p = os.path.join(self._name, pathname)
+ list.append(SlimeFolder_Unix(p))
+ self._subfolders = list
+
+ def add_message(self, file):
+ self._assert_open()
+
+ dummy = mhlib.Message('NONE', 1, file)
+ self._fp.seek(0,2)
+ start = self._fp.tell()
+ self._fp.write(self._make_separator(dummy))
+ del dummy
+ file.seek(0,0)
+ while 1:
+ buf = file.read(16*1024)
+ if not buf:
+ break
+ self._fp.write(buf)
+ end = self._fp.tell()
+ subfile = _Subfile(self._fp, start, end)
+ msg = SlimeMessage_Unix(self, subfile)
+ self._messages.append(msg)
+ return msg
+
+ def commit_changes(self):
+ self._assert_open()
+ if not self._is_dir and self._dirty:
+ newname = self._name + ".new"
+ f = open(newname, "w")
+ for msg in self._messages:
+ if not msg.has_status("D"):
+ f.write(self._make_separator(msg))
+ f.write(msg.getfulltext())
+ f.close()
+ os.rename(newname, self._name)
+
+ self._messages = []
+ self._dirty = 0
+ self.open()
+ self.rescan_messages()
+ self.rescan_subfolders()
+
+ def _make_separator(self, msg):
+ date = msg.getdate("date")
+ if date is None:
+ date = (1970,1,1,0,0,0,3,1,0)
+ mon = ["", "Jan", "Feb", "Mar", "Apr", "May",
+ "Jun", "Jul", "Aug", "Sep", "Oct",
+ "Nov", "Dec"][date[1]]
+ dow = ["Mon", "Tue", "Wed", "Thu", "Fri",
+ "Sat", "Sun"][date[6]]
+ year = date[0]
+ if year < 1900:
+ year = year + 1900
+ name, addr = msg.getaddr("from")
+ if not addr:
+ addr = "foo"
+ return "From %s %s %s %2d %2d:%02d:%02d %d\n" % \
+ (addr, dow, mon, date[2], date[3], date[4],
+ date[5], year)
+
+class SlimeMessage_Unix(SlimeMessage):
+ """A SlimeMessage interface to messages in Unix mailbox folders."""
+
+ def __init__(self, folder, subfile):
+ SlimeMessage.__init__(self, folder)
+ self._start = subfile.start
+ self._stop = subfile.stop
+ self._checksum = self._start
+
+ def _open(self):
+ self._folder._assert_open()
+ self._open_count = self._open_count + 1
+ if not self._msg:
+ self._fp = _Subfile(self._folder._fp,
+ self._start, self._stop)
+ self._msg = mhlib.Message('NONE', 1, self._fp)
+
+ # make sure stuff is cached
+ if not self._cached_date:
+ self._cache_headers()
+
+ def _close(self):
+ self._folder._assert_open()
+ self._open_count = self._open_count - 1
+ if self._open_count == 0:
+ self._msg = None
+ self._fp.close()
+
+def main():
+ f = SlimeFolder_Unix(os.path.expanduser("~/mail"))
+ f.open()
+ f.rescan_subfolders()
+ for sub in f.list_all_subfolders():
+ sub.open()
+ sub.rescan_messages()
+ for msg in sub.list_all_messages()[:2]:
+ print sub._name, msg["subject"]
+ sub.close()
+ print
+ f.close()
+
+def profile_main():
+ import time
+ f = unix_top_folder("~/mail")
+ f.open()
+ f.rescan_subfolders()
+ for sub in f.list_all_subfolders():
+ if repr(sub) == "/home/liw/mail/yow3":
+ sub.open()
+ print "computing threads"
+ start = time.time()
+ sub.rescan_messages()
+ sub.list_all_threads()
+ end = time.time()
+ print "threads computed", end - start
+ sub.close()
+ f.close()
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/ui.py b/slime-0.11/ui.py
new file mode 100644
index 0000000..016f5ca
--- /dev/null
+++ b/slime-0.11/ui.py
@@ -0,0 +1,1058 @@
+from Tkinter import *
+from ScrolledText import ScrolledText
+
+import string, time, tempfile
+import Shells, Menu, ButtonBox
+import slime_abstract, slime_pgpmime, slime_send, ui_config
+config = ui_config.config
+
+from ui_helpers import *
+from ui_compose import *
+from slime_folderinfo import *
+
+INITIAL_INBOX_SCAN_DELAY = 5*1000
+
+folderinfo_file = os.path.expanduser("~/.slime/folderinfo")
+
+root = None
+draft_folder = None
+fcc_folder = None
+
+pgp_password = None
+pgp_timestamp = 0
+
+class DummyFeedback:
+ def delete_msg(self, foo, msg, msgwin): pass
+ def undelete_msg(self, foo, msg, msgwin): pass
+ def close_msgwin(self, foo, msgwin): pass
+ def next_msg(self, foo, msg, msgwin): pass
+ def prev_msg(self, foo, msg, msgwin): pass
+ def mark_unread_msg(self, foo, msg, msgwin): pass
+
+ def send_msg(self, msg): pass
+
+class MessageWindow:
+
+ _all_msgwin = []
+
+ def __init__(self):
+ self._all_msgwin.append(self)
+
+ self.top = Toplevel()
+ self.top.title("Slime Message Window")
+ self.top.iconname("Slime Message Window")
+
+ self.feedback = DummyFeedback()
+ self.shorten_headers = 1
+
+ self.top.bind("n", self.next_msg)
+ self.top.bind("p", self.prev_msg)
+ self.top.bind("d", self.delete_msg)
+ self.top.bind("u", self.undelete_msg)
+ self.top.bind("<Key-Down>", self.next_line)
+ self.top.bind("<Key-Up>", self.prev_line)
+ self.top.bind("<Key-Next>", self.next_page)
+ self.top.bind("<Key-Prior>", self.prev_page)
+ self.top.bind("<Key-space>", self.next_page_or_msg)
+
+ menubar = Menu.Menubar(self.top)
+ Menu.Pulldown(menubar, "Menu",
+ ["Reply", self.reply_to_msg],
+ ["-"],
+ ["Delete", self.delete_msg],
+ ["Undelete", self.undelete_msg],
+ ["Mark unread", self.undelete_msg],
+ ["-"],
+ ["Next", self.next_msg],
+ ["Prev", self.prev_msg],
+ ["-"],
+ ["Show selected headers", self.show_important_headers],
+ ["Show full headers", self.show_full_headers],
+ ["-"],
+ ["Close", self.close_msgwin])
+
+ buttonbar = Frame(self.top)
+ buttonbar.pack(fill=X)
+ CommandButton(buttonbar, "Delete", self.delete_msg)
+
+ self.textwin = ScrolledText(self.top)
+ self.textwin.pack(expand=YES, fill=BOTH)
+ self.textwin.config(background="white")
+ self.textwin.config(font=config["normal-font"])
+ self.disable()
+
+ statusbar = Frame(self.top)
+ statusbar.pack(fill=X)
+ b = Label(statusbar, text="Status:")
+ b.pack(side=LEFT)
+ self.statuswin = Label(statusbar, text="")
+ self.statuswin.config(relief=SUNKEN)
+ self.statuswin.pack(side=LEFT)
+ b = Label(statusbar, text="Size:")
+ b.pack(side=LEFT)
+ self.sizewin = Label(statusbar, text="", relief=SUNKEN)
+ self.sizewin.pack(side=LEFT)
+
+ self.feedback = None
+ self.msg = None
+
+ def show_full_headers(self):
+ self.shorten_headers = 0
+ self.re_set()
+
+ def show_important_headers(self):
+ self.shorten_headers = 1
+ self.re_set()
+
+ def place(self, x, y):
+ self.top.geometry("+%d+%d" % (x,y))
+
+ def enable(self):
+ self.textwin.config(state="normal")
+
+ def disable(self):
+ self.textwin.config(state="disabled")
+
+ def reply_to_msg(self, event=None):
+ self.feedback.reply_to_msg(self.msg)
+
+ def delete_msg(self, event=None):
+ self.feedback.delete_msg(event, self.msg, self)
+
+ def undelete_msg(self, event=None):
+ self.feedback.undelete_msg(event, self.msg, self)
+
+ def close_msgwin(self, event=None):
+ self.feedback.close_msgwin(self)
+ self.top.destroy()
+ self._all_msgwin.remove(self)
+
+ def next_msg(self, event=None):
+ self.feedback.next_msg(event, self.msg, self)
+
+ def prev_msg(self, event=None):
+ self.feedback.prev_msg(event, self.msg, self)
+
+ def get_top_index(self):
+ index = self.textwin.index("@0,0")
+ pair = string.split(index, ".")
+ return string.atoi(pair[0]), string.atoi(pair[1])
+
+ def height(self):
+ return string.atoi(self.textwin.cget("height"))
+
+ def next_line(self, event=None):
+ line, col = self.get_top_index()
+ self.textwin.yview(line)
+
+ def prev_line(self, event=None):
+ line, col = self.get_top_index()
+ line = line - 1
+ if line < 0:
+ line = 0
+ self.textwin.yview(line-1)
+
+ def next_page(self, event=None):
+ line, col = self.get_top_index()
+ line = line + (self.height() - 2)
+ self.textwin.yview(line-1)
+
+ def next_page_or_msg(self, event=None):
+ line, col = self.get_top_index()
+ line2 = line + (self.height() - 2)
+ if line2 >= self.lines() - 1:
+ self.next_msg()
+ else:
+ self.textwin.yview(line2-1)
+
+ def prev_page(self, event=None):
+ line, col = self.get_top_index()
+ line = line - (self.height() - 2)
+ if line < 1:
+ line = 1
+ self.textwin.yview(line-1)
+
+ def mark_unread_msg(self, event=None):
+ self.feedback.mark_unread_msg(event, self.msg, self)
+
+ def set(self, msg, feedback):
+ if feedback:
+ self.feedback = feedback
+ else:
+ self.feedback = DummyFeedback()
+ self.msg = msg
+ self.enable()
+ self.textwin.delete('0.0', 'end')
+ for tag in self.textwin.tag_names():
+ self.textwin.tag_delete(tag)
+ self.append_part(msg)
+ self.append_separator()
+ self.disable()
+ x = "%.20s: %.30s" % (msg["from"], msg["subject"])
+ self.top.title(x)
+ self.top.iconname(x)
+ self.update_status()
+
+ def re_set(self):
+ self.set(self.msg, self.feedback)
+
+ def update_status(self):
+ for msgwin in self._all_msgwin:
+ x = msgwin.msg.get_status()
+ if x == "":
+ x = " "
+ msgwin.statuswin.config(text=x)
+ msgwin.sizewin.config(text="%d" % self.lines())
+
+ def append_part(self, part):
+ self.append_headers(part)
+ type = part.getmaintype()
+ subtype = part.getsubtype()
+ if type == "multipart":
+ if subtype == "signed":
+ self.append_multipart_signed(part)
+ else:
+ self.append_multipart_any(part)
+ elif type == "image" and subtype == "gif":
+ self.append_separator()
+ self.append_image_gif(part)
+ else:
+ self.append_separator()
+ self.append_text_any(part)
+
+ def append_multipart_signed(self, part):
+ if ui_config.config["check-pgp-signatures"]:
+ try:
+ slime_pgpmime.check_signature(part)
+ self.append_line("Signature: OK\n")
+ except slime_pgpmime.SecretMissing, detail:
+ self.append_error("Signature: Secret key is missing\n")
+ except slime_pgpmime.PublicMissing, detail:
+ self.append_error("Signature: Public key is missing\n")
+ except slime_pgpmime.GoodUntrustedSignature, detail:
+ self.append_error("Signature: Good, but untrusted signature\n")
+ except slime_pgpmime.BadUntrustedSignature, detail:
+ self.append_error("Signature: Bad signature\n")
+ except slime_pgpmime.BadTrustedSignature, detail:
+ self.append_error("Signature: Bad signature\n")
+ except slime_pgpmime.OtherResult, detail:
+ self.append_error("Signature: Unknown result\n")
+ else:
+ self.append_line("Signature: unchecked\n")
+ self.append_multipart_any(part)
+
+ def append_multipart_any(self, part):
+ for p in part.getbodyparts():
+ self.append_part(p)
+
+ def append_error(self, line):
+ self.textwin.insert('end', line)
+ tag = self.tag_last_line('error')
+ self.textwin.tag_config(tag, background='red')
+
+ def append_line(self, line):
+ self.textwin.insert('end', line)
+
+ def append_image_gif(self, part):
+ data = part.getbodytext()
+ tempname = tempfile.mktemp()
+ helpers.write_text_to_named_file(tempname, data)
+ photo = PhotoImage(file=tempname)
+ label = Label(master=self.textwin, image=photo)
+ label.pack()
+ self.textwin.window_create('end', window=label)
+ os.remove(tempname)
+
+ def append_text_any(self, part):
+ self.textwin.insert('end', part.getbodytext())
+
+ def append_separator(self):
+ str = ""
+ self.textwin.insert("end", str + "\n")
+ tag = self.tag_last_line("sep")
+ self.textwin.tag_config(tag, background='lightgrey',
+ relief=RAISED, borderwidth=2, font='5x7')
+
+ def lines(self):
+ index = self.textwin.index("end")
+ pair = string.split(index, ".")
+ return string.atoi(pair[0]) - 2
+
+ def tag_last_line(self, tag_prefix):
+ lines = self.lines()
+ tag = "%s-%d" % (tag_prefix, lines)
+ self.textwin.tag_add(tag, "%d.0" % lines, "%d.0" % (lines + 1))
+ return tag
+
+ def append_headers(self, part):
+ if self.shorten_headers:
+ h = []
+ headers = string.split(config["important-headers"])
+ for header in headers:
+ try:
+ v = part[header]
+ if v:
+ h.append("%s: %s\n" %
+ (header, v))
+ except KeyError:
+ pass
+ h = string.join(h, "")
+ self.textwin.insert('end', h)
+ else:
+ self.textwin.insert('end', part.getheadertext())
+
+class SelectionList(ScrolledText):
+ def __init__(self, master):
+ ScrolledText.__init__(self, master)
+ self.pack(side=LEFT, expand=YES, fill=BOTH)
+ self.config(wrap="none", spacing1=1, exportselection=NO)
+ self.config(background="white")
+ self.config(font=config["normal-font"])
+ self.bind("<Button-1>", self.select)
+ self.bind("<Double-Button-1>", self.select_double)
+ self.bind("<Button-2>", self.select2)
+ self.bind("<Double-Button-2>", self.select2_double)
+ self.bind("<Button-3>", self.select3)
+ self.bind("<Double-Button-3>", self.select3_double)
+ self.disable()
+ self.current = None
+
+ def get_top_index(self):
+ index = self.index("@0,0")
+ pair = string.split(index, ".")
+ return string.atoi(pair[0]), string.atoi(pair[1])
+
+ def enable(self):
+ self.config(state="normal")
+
+ def disable(self):
+ self.config(state="disabled")
+
+ def select_hook(self):
+ pass
+ def double_hook(self):
+ pass
+
+ def select2_hook(self, lineno):
+ pass
+ def double2_hook(self, lineno):
+ pass
+
+ def select3_hook(self, lineno):
+ pass
+ def double3_hook(self, lineno):
+ pass
+
+ def update_cursor(self):
+ if not self.current is None:
+ self.tag_delete("curline")
+ self.tag_add("curline",
+ "%d.0" % self.current,
+ "%d.0 lineend + 1 chars" % (self.current))
+ self.tag_config("curline", background="darkgrey")
+
+ def select_by_lineno(self, lineno):
+ self.prev_current = self.current
+ self.current = lineno
+ self.select_hook()
+ self.update_cursor()
+ self.see("%d.0" % (lineno + 4))
+ self.see("%d.0" % (lineno))
+
+ def _get_lineno(self, event):
+ index = self.index("@%d,%d" % (event.x, event.y))
+ pair = string.split(index, ".")
+ return string.atoi(pair[0])
+
+ def select(self, event):
+ self.select_by_lineno(self._get_lineno(event))
+ def select_double(self, event):
+ self.double_hook()
+
+ def select2(self, event):
+ self.select2_hook(self._get_lineno(event))
+ def select2_double(self, event):
+ self.double2_hook(self._get_lineno(event))
+
+ def select3(self, event):
+ self.select3_hook(self._get_lineno(event))
+ def select3_double(self, event):
+ self.double3_hook(self._get_lineno(event))
+
+class FolderTocWindow(SelectionList):
+ def __init__(self, master):
+ SelectionList.__init__(self, master)
+ self.config(width=60, height=15)
+ self.folder = None
+ self.msg = None
+ self.msgwin = None
+ self.msg_dict = {}
+ self.msg_count = 0
+
+ self.focus_set()
+ self.bind("n", self.next_msg)
+ self.bind("p", self.prev_msg)
+ self.bind("d", self.delete_msg)
+ self.bind("u", self.undelete_msg)
+
+ def set(self, folder, mcount_win, folder_win):
+ self.enable()
+ self.delete('0.0', 'end')
+ n = 0
+ self.folder = folder
+ self.msg = None
+ self.msg_dict = {}
+ self.msg_count = len(folder.list_all_threads())
+ for indent_level, msg in folder.list_all_threads():
+ if n > 0:
+ self.insert('end', '\n')
+ n = n + 1
+ self.msg_dict[msg] = n
+
+ parts = self.make_toc_parts(indent_level, msg)
+ line = self.make_toc_line(parts)
+ self.insert('end', line)
+ self.set_tags(parts, line, n)
+ self.disable()
+ self.mcount_win = mcount_win
+ self.mcount_win.config(text="%d" % self.msg_count)
+ self.folder_win = folder_win
+
+ def re_set(self):
+ if self.folder:
+ old_msg = self.msg
+ self.folder.rescan_messages()
+ self.set(self.folder, self.mcount_win, self.folder_win)
+ if self.msg_dict.has_key(old_msg):
+ self.select_by_lineno(self.msg_dict[old_msg])
+ elif self.msgwin:
+ self.msgwin.close_msgwin()
+
+ def make_toc_line(self, parts):
+ return "%s %s %-20.20s %s%s" % \
+ (parts[0], parts[4], parts[3], parts[1], parts[2])
+
+ def make_toc_parts(self, indent_level, msg):
+ if msg.has_status("N"):
+ status = "N"
+ elif msg.has_status("D"):
+ status = "D"
+ else:
+ status = " "
+ if indent_level <= config["max-subject-indent"]:
+ indent = "%*s" % (indent_level * 2, "")
+ else:
+ indent = "%*s(%d) " % \
+ (config["max-subject-indent"] * 2, "",
+ indent_level)
+ name, addr = msg.getaddr("from")
+ if not name:
+ name = addr
+ subject = msg["subject"]
+ if subject:
+ subject = string.join(string.split(subject, "\n"), " ")
+ else:
+ subject = "<none>"
+ date_tuple = msg.getdate("date")
+ if date_tuple is None:
+ date = "%10s" % ""
+ else:
+ if date_tuple[0] < 1900:
+ year = 1900 + date_tuple[0]
+ else:
+ year = date_tuple[0]
+ date = "%04d-%02d-%02d" % \
+ (year, date_tuple[1], date_tuple[2])
+
+ return status, indent, subject, name, date
+
+ def set_tags(self, parts, line, lineno):
+# sub1 = len(parts[0]) + 1 + len(parts[4]) + 22 + len(parts[1])
+# sub2 = sub1 + len(parts[2])
+
+ sub1 = 0
+ sub2 = len(line)
+
+ if parts[0] == "N":
+ font = config["unread-font"]
+ else:
+ font = config["normal-font"]
+ self.add_tag(".subject", lineno, sub1, sub2, font)
+
+ def add_tag(self, suffix, lineno, fromcol, tocol, font):
+ tag = "%d.%s" % (lineno, suffix)
+ self.tag_add(tag,
+ "%d.%d" % (lineno, fromcol),
+ "%d.%d" % (lineno, tocol))
+ self.tag_configure(tag, font=font)
+
+ def update_toc_line(self, msg):
+ if not self.msg_dict.has_key(msg):
+ return
+ lineno = self.msg_dict[msg]
+ parts = self.make_toc_parts(self.folder.get_msg_level(msg), msg)
+ line = self.make_toc_line(parts)
+ self.enable()
+ self.delete("%d.0" % lineno, "%d.end" % lineno)
+ self.insert("%d.0" % lineno, line)
+ self.disable()
+ self.set_tags(parts, line, lineno)
+ self.folder_win.redraw_folder(self.folder)
+
+ def select_hook(self):
+ foo, self.msg = self.folder.list_all_threads()[self.current-1]
+ if self.msg.has_status("N"):
+ self.msg.remove_status("N")
+ self.folder_win.decrement_unread()
+
+ if not self.msgwin:
+ self.msgwin = MessageWindow()
+ self.put_at_good_position(self.msgwin)
+ self.msgwin.set(self.msg, self)
+ self.update_toc_line(self.msg)
+ self.update_cursor()
+
+ def put_at_good_position(self, msgwin):
+ x = self.winfo_rootx()
+ y = self.winfo_rooty()
+ w = self.winfo_width()
+ h = self.winfo_height()
+ msgwin.place(x+w/10, y+h+h/10)
+
+ def double_hook(self):
+ msg = self.folder.list_all_messages()[self.current-1]
+ msgwin = MessageWindow()
+ msgwin.set(msg, self)
+
+ def delete_msg(self, event=None, msg=None, msgwin=None):
+ if msg is None:
+ msg = self.msg
+ msgwin = self.msgwin
+ if msg:
+ msg.delete_self()
+ msgwin.update_status()
+ if self.msg_dict.has_key(msg):
+ self.update_toc_line(msg)
+ self.update_cursor()
+ self.next_msg(event, msg, msgwin)
+
+ def undelete_msg(self, event=None, msg=None, msgwin=None):
+ if msg is None:
+ msg = self.msg
+ msgwin = self.msgwin
+ msg.undelete_self()
+ if self.msg_dict.has_key(msg):
+ self.update_toc_line(msg)
+ self.update_cursor()
+
+ def mark_unread_msg(self, event=None, msg=None, msgwin=None):
+ if msg is None:
+ msg = self.msg
+ msgwin = self.msgwin
+ msg.give_status("N")
+ if self.msg_dict.has_key(msg):
+ self.update_toc_line(msg)
+ self.update_cursor()
+
+ def next_msg(self, event=None, msg=None, msgwin=None):
+ if msg is None:
+ msg = self.msg
+ msgwin = self.msgwin
+ if msgwin == self.msgwin and self.current < self.msg_count:
+ self.select_by_lineno(self.current+1)
+
+ def prev_msg(self, event=None, msg=None, msgwin=None):
+ if msg is None:
+ msg = self.msg
+ msgwin = self.msgwin
+ if msgwin == self.msgwin and self.current > 1:
+ self.select_by_lineno(self.current-1)
+
+ def close_msgwin(self, msgwin):
+ if msgwin == self.msgwin:
+ self.msgwin = None
+
+ def compose_msg(self):
+ if self.folder == draft_folder and self.msg:
+ c = ComposeWindow(self, self.msg, self)
+ c.show()
+ else:
+ file = StringIO.StringIO(slime_send.make_new_message_text())
+ msg = draft_folder.add_message(file)
+ self.refresh_draft_toc()
+ c = ComposeWindow(self, msg, self)
+ c.show()
+
+ def reply_to_msg(self, origmsg):
+ txt = slime_send.make_reply_message_text(origmsg)
+ file = StringIO.StringIO(txt)
+ msg = draft_folder.add_message(file)
+ self.refresh_draft_toc()
+ c = ComposeWindow(self, msg, self)
+ c.show()
+
+ def refresh_draft_toc(self):
+ draft_folder.commit_changes()
+ if self.folder == draft_folder:
+ self.re_set()
+
+ def get_pgp_authentication(self, msg):
+ global pgp_password, pgp_timestamp
+
+ if config["pgp-username"]:
+ username = config["pgp-username"]
+ else:
+ d = PgpUsernameDialog(self, msg["from"])
+ username = d.show()
+ if not username:
+ return None, None
+
+ now = time.time()
+ diff = now - pgp_timestamp
+ if diff < 0 or diff > 60*config["pgp-max-password-age"]:
+ p = PasswordDialog(self)
+ password = p.show()
+ if not password:
+ return None, None
+ pgp_password = password
+ pgp_timestamp = now
+ else:
+ password = pgp_password
+
+ return username, password
+
+ def send_msg(self, msg):
+ if config["make-pgp-signatures"]:
+ username, password = self.get_pgp_authentication(msg)
+ if username is None:
+ return 0
+ try:
+ new_text = slime_pgpmime.make_signature(msg,
+ username, password)
+ except slime_pgpmime.OtherResult, detail:
+ d = ErrorBox(self, "PGP failed", detail[2])
+ d.show()
+ return 0
+ msg.change_text(StringIO.StringIO(new_text))
+ slime_send.prepare_for_send(msg)
+ slime_send.send_msg(msg)
+ if self.store_to_fcc_folder(msg) == 0:
+ return 0
+ msg.delete_self()
+ self.refresh_draft_toc()
+ return 1
+
+ def store_to_fcc_folder(self, msg):
+ global fcc_folder, tops
+ if not config["fcc-folder"]:
+ return 1
+ if not fcc_folder:
+ fcc_folder = \
+ helpers.find_folder_by_name(config["fcc-folder"],
+ root)
+ if not fcc_folder:
+ d = ErrorBox(self, "No FCC folder",
+ ("There is no folder named\n%s\n" +
+ "No copy of message has been saved") \
+ % (config["fcc-folder"]))
+ d.show()
+ return 0
+ try:
+ was_open = fcc_folder.is_open()
+ if not was_open:
+ fcc_folder.open()
+ fcc_folder.rescan_messages()
+ fcc_folder.copy_message_here(msg)
+ if not was_open:
+ fcc_folder.close()
+ except slime_abstract.Error, detail:
+ d = ErrorBox(self, "FCC copy failed",
+ "Saving copy of sent message failed:\n" + \
+ detail)
+ return 0
+ return 1
+
+class FolderWindow(SelectionList):
+ def __init__(self, master):
+ SelectionList.__init__(self, master)
+ self.config(width=25, height=15)
+ self.pack(expand=NO)
+ self.folder_to_lineno = {}
+ self.folder = None
+ self.msgwin = None
+ self.msg = None
+ self.main_win = None
+
+ def decrement_unread(self):
+ if self.main_win:
+ self.main_win.decrement_unread()
+
+ def get_folders(self, root, indent=0, recurse=1):
+ list = []
+ for sub in root.list_all_subfolders():
+ list.append(sub.get_nice_name(), sub)
+ list.sort()
+ result = []
+ for tuple in list:
+ sub = tuple[1]
+ result.append(indent, sub)
+ if recurse:
+ sub.open()
+ sub.rescan_messages()
+ sub.rescan_subfolders()
+ result = result + self.get_folders(sub,
+ indent+1, recurse-1)
+ sub.close()
+ return result
+
+ def format_folder(self, level, folder, count_width, max_folder_indent):
+ is_inbox = (get_folderinfo(repr(folder), "inbox") == "yes")
+ if is_inbox:
+ was_open = folder.is_open()
+ if not was_open:
+ folder.open()
+ folder.rescan_messages()
+ msgs = folder.list_all_messages()
+ else:
+ was_open = 1
+ msgs = []
+
+ if is_inbox:
+ cnt = "%*s" % (count_width, len(msgs))
+ else:
+ cnt = "%*s" % (count_width, "")
+
+ if level <= max_folder_indent:
+ indent = "%*s" % (level*2, "")
+ else:
+ indent = "%*s(%d) " % (max_folder_indent*2, "", level)
+ str = "%s %s%s" % (cnt, indent, folder.get_nice_name())
+
+ font = config["normal-font"]
+ for msg in msgs:
+ if msg.has_status("N"):
+ font = config["unread-font"]
+
+ if is_inbox and not was_open:
+ folder.close()
+
+ return str, font
+
+ def draw_folders(self):
+ line, col = self.get_top_index()
+ self.enable()
+ self.delete('0.0', 'end')
+ self.folder_to_lineno = {}
+ lineno = 1
+ for level, f in self.folders:
+ self.folder_to_lineno[f] = lineno
+ s, font = self.format_folder(level, f,
+ config["count-width"],
+ config["max-folder-indent"])
+ if lineno > 1:
+ self.insert('end', '\n')
+ self.insert('end', s)
+ self.change_font_on_line(lineno, font)
+ lineno = lineno + 1
+ self.disable()
+ try:
+ self.current = self.folder_to_lineno[self.folder]
+ except KeyError:
+ self.current = None
+ self.update_cursor()
+ self.yview(line-1)
+ self.fcount_win.config(text="%d" % len(self.folders))
+
+ def change_font_on_line(self, lineno, font):
+ tag = "%d.font" % (lineno)
+ self.tag_delete(tag)
+ self.tag_add(tag,
+ "%d.0" % (lineno),
+ "%d.0 lineend" % (lineno))
+ self.tag_config(tag, font=font)
+
+ def redraw_folder(self, folder):
+ if self.folder_to_lineno.has_key(folder):
+ lineno = self.folder_to_lineno[folder]
+ for level, f in self.folders:
+ if f == folder:
+ break
+ s, font = self.format_folder(level, folder,
+ config["count-width"],
+ config["max-folder-indent"])
+ self.enable()
+ self.delete("%d.0" % lineno, "%d.0 lineend" % lineno)
+ self.insert("%d.0" % lineno, s)
+ self.change_font_on_line(lineno, font)
+ self.disable()
+ if self.folder == folder:
+ self.update_cursor()
+
+ def set(self, slime_root, toc_win, fcount_win, mcount_win, main_win):
+ self.slime_root = slime_root
+ self.toc_win = toc_win
+ self.fcount_win = fcount_win
+ self.mcount_win = mcount_win
+ self.folders = self.get_folders(slime_root, recurse=0)
+ self.draw_folders()
+ self.main_win = main_win
+
+ def show_subfolders(self, index):
+ this = self.folders[index]
+ was_open = this[1].is_open()
+ if not was_open:
+ this[1].open()
+ this[1].rescan_subfolders()
+ subs = self.get_folders(this[1], this[0]+1, recurse=0)
+ if not was_open:
+ this[1].close()
+ self.folders = self.folders[:index+1] + subs + \
+ self.folders[index+1:]
+ self.draw_folders()
+
+ def hide_subfolders(self, index):
+ this = self.folders[index]
+ next = self.folders[index+1]
+ i = index+1
+ while i < len(self.folders) and this[0] < self.folders[i][0]:
+ i = i + 1
+ self.folders = self.folders[:index+1] + self.folders[i:]
+ self.draw_folders()
+
+ def show_or_hide_subfolders(self, index=None):
+ if index is None:
+ index = self.current-1
+ this = self.folders[index]
+ if index+1 < len(self.folders):
+ next = self.folders[index+1]
+ if this[0] < next[0]:
+ self.hide_subfolders(index)
+ else:
+ self.show_subfolders(index)
+ else:
+ self.show_subfolders(index)
+
+ def select_hook(self):
+ new_folder = self.folders[self.current-1][1]
+ if not self.folder:
+ self.folder = new_folder
+ self.folder.open()
+ self.folder.rescan_subfolders()
+ self.toc_win.set(self.folder, self.mcount_win, self)
+ self.show_or_hide_subfolders()
+ elif self.folder == new_folder:
+ self.folder.rescan_subfolders()
+ self.toc_win.re_set()
+ self.show_or_hide_subfolders()
+ else:
+ if self.commit_changes(re_set = 0):
+ self.folder.close()
+ self.folder = new_folder
+ self.folder.open()
+ self.folder.rescan_subfolders()
+ self.folder.rescan_messages()
+ self.toc_win.set(self.folder, self.mcount_win,
+ self)
+ self.show_or_hide_subfolders()
+ else:
+ self.current = self.prev_current
+
+ def commit_changes(self, re_set = 1):
+ if self.folder and not self.folder.is_clean():
+ d = YesNoDialog(self,
+ "Commit folder %s?" % \
+ self.folder.get_nice_name())
+ doit = d.show()
+ d.destroy()
+ if doit:
+ self.folder.commit_changes()
+ self.redraw_folder(self.folder)
+ if re_set:
+ self.toc_win.re_set()
+ return 1
+ return 0
+ return 1
+
+ def select2_hook(self, lineno):
+ self.show_or_hide_subfolders(lineno-1)
+
+ def select3_hook(self, lineno):
+ if not self.folder:
+ print "no open folder"
+ elif not self.toc_win.msg:
+ print "no selected message"
+ else:
+ target = self.folders[lineno-1][1]
+ was_open = target.is_open()
+ if not was_open:
+ target.open()
+ target.rescan_messages()
+ newmsg = target.move_message_here(self.toc_win.msg)
+ if not was_open:
+ target.close()
+ self.toc_win.delete_msg()
+
+class MainWindow(Frame):
+ default_toplevel_used = 0
+ def __init__(self, slime_root):
+ if self.default_toplevel_used:
+ master = Toplevel()
+ else:
+ master = None
+ self.default_toplevel_used = 1
+ Frame.__init__(self, master)
+ self.pack(expand=YES, fill=BOTH)
+ t = self.winfo_toplevel()
+ t.title("Slime")
+ t.iconname("Slime")
+
+ menubar = Menu.Menubar(self)
+
+ self.slime_root = slime_root
+ f = Frame(self)
+ f.pack(expand=YES, fill=BOTH)
+ self.folder_win = FolderWindow(f)
+ self.ftoc_win = FolderTocWindow(f)
+
+ statusbar = Frame(self)
+ statusbar.pack(fill=X)
+ b = Label(statusbar, text="Folders:")
+ b.pack(side=LEFT)
+ self.fcount_win = Label(statusbar, text="", relief=SUNKEN)
+ self.fcount_win.pack(side=LEFT)
+
+ b = Label(statusbar, text="Unread:")
+ b.pack(side=LEFT)
+ self.total_unread_win = Label(statusbar, text="", relief=SUNKEN)
+ self.total_unread_win.pack(side=LEFT)
+
+ self.mcount_win = Label(statusbar, text="", relief=SUNKEN)
+ self.mcount_win.pack(side=RIGHT)
+ b = Label(statusbar, text="Messages:")
+ b.pack(side=RIGHT)
+
+ self.folder_win.set(self.slime_root, self.ftoc_win,
+ self.fcount_win, self.mcount_win, self)
+
+ Menu.Pulldown(menubar, "&Folder",
+ ["Scan inboxes", self.scan_inboxes],
+ ["Rescan folder", self.ftoc_win.re_set],
+ ["Commit folder", self.folder_win.commit_changes],
+ ["-"],
+ ["Compose message", self.ftoc_win.compose_msg],
+ ["-"],
+ ["Edit folder options", self.edit_folderinfo],
+ ["-"],
+ ["Options", ui_config.edit_config],
+ ["Save options", self.save_config],
+ ["-"],
+ ["Exit", self.quit_program])
+
+ self.unread = 0
+ self.after(INITIAL_INBOX_SCAN_DELAY, self.do_inbox_scan)
+
+ def save_config(self):
+ ui_config.save_config()
+ d = InfoBox(self, "Config saved", "Configuration has been saved")
+ d.show()
+
+ def edit_folderinfo(self):
+ if self.folder_win.folder is None:
+ return
+ name = repr(self.folder_win.folder)
+ is_inbox = (get_folderinfo(name, "inbox") == "yes")
+ d = FolderInfoEditor(self, name, is_inbox)
+ is_inbox = d.show()
+ if is_inbox is None:
+ return
+ if is_inbox:
+ set_folderinfo(name, "inbox", "yes")
+ else:
+ set_folderinfo(name, "inbox", "no")
+ self.folder_win.redraw_folder(self.folder_win.folder)
+
+ def do_inbox_scan(self):
+ self.scan_inboxes()
+ self.after(config["inbox-scan-frequency"]*60*1000,
+ self.do_inbox_scan)
+
+ def scan_inboxes(self):
+ global root
+ names = get_folders_with_info("inbox", "yes")
+ helpers.clear_name_to_folder_cache()
+ unread = 0
+ for name in names:
+ folder = helpers.find_folder_by_name(name, root)
+ was_open = folder.is_open()
+ if not was_open:
+ folder.open()
+ folder.rescan_messages()
+ folder.rescan_subfolders()
+ for msg in folder.list_all_messages():
+ if msg.has_status("N"):
+ unread = unread + 1
+ self.folder_win.redraw_folder(folder)
+ if not was_open:
+ folder.close()
+ helpers.clear_name_to_folder_cache()
+ self.set_unread(unread)
+
+ def set_unread(self, unread):
+ self.unread = unread
+ self.total_unread_win.config(text="%d" % unread)
+
+ def decrement_unread(self):
+ self.set_unread(self.unread - 1)
+
+ def quit_program(self):
+ d = YesNoDialog(self, "Really exit?")
+ if d.show():
+ self.folder_win.commit_changes()
+ self.quit()
+
+def main():
+ from slime_mh import *
+ from slime_unix import *
+ from slime_root import *
+ from slime_draft import *
+ import os
+ global draft_folder, root
+
+ ui_config.load_config()
+ load_folderinfo(folderinfo_file)
+
+ root = SlimeFolder_Root()
+ root.open()
+
+ draft_folder = make_draft_folder()
+ draft_folder.open()
+ draft_folder.rescan_messages()
+ tops = [draft_folder]
+ maybe_tops = [mh_top_folder(),
+ unix_top_folder("~/Mail"),
+ unix_top_folder("~/mail")]
+ for f in maybe_tops:
+ if f:
+ tops.append(f)
+ if len(tops) == 1:
+ tops[0].open()
+ tops[0].rescan_messages()
+ tops[0].rescan_subfolders()
+ if len(tops[0].list_all_messages()) == 0:
+ for f in tops[0].list_all_subfolders():
+ root.add_subfolder(f)
+ tops[0].close()
+ tops = []
+ else:
+ tops[0].close()
+ for f in tops:
+ root.add_subfolder(f)
+
+ mainwin = MainWindow(root)
+ mainwin.mainloop()
+ save_folderinfo(folderinfo_file)
+ draft_folder.close()
+ root.close()
+
+if __name__ == "__main__":
+ main()
diff --git a/slime-0.11/ui_compose.py b/slime-0.11/ui_compose.py
new file mode 100644
index 0000000..1fd970b
--- /dev/null
+++ b/slime-0.11/ui_compose.py
@@ -0,0 +1,96 @@
+from Tkinter import *
+from ScrolledText import *
+from ui_helpers import *
+from ui_config import config
+
+import Shells, Menu, StringIO, helpers, spawn, os, tempfile
+
+class ComposeWindow(Shells.NonModal):
+ def __init__(self, master, msg, feedback):
+ Shells.NonModal.__init__(self, master)
+
+ self.msg = msg
+ self.feedback = feedback
+
+ self.top.title("Slime Composition Window")
+ self.top.iconname("Slime Composition Window")
+
+ menubar = Menu.Menubar(self.top)
+ Menu.Pulldown(menubar, "Menu",
+ ["Use external editor", self.use_external],
+ ["Save", self.save_msg],
+ ["Postpone", self.postpone_msg],
+ ["Send", self.send_msg],
+ ["Abort", self.abort_msg])
+
+ buttonbar = Frame(self.top)
+ buttonbar.pack(fill=X)
+ CommandButton(buttonbar, "Send", self.send_msg)
+
+ self.textwin = ScrolledText(self.top)
+ self.textwin.pack(expand=YES, fill=BOTH)
+ self.textwin.focus_set()
+ self.textwin.config(background="white")
+ self.textwin.config(font=config["normal-font"])
+
+ self.textwin.insert('0.0', self.msg.getfulltext())
+ self.textwin.mark_set('insert', '0.0')
+
+ def use_external(self):
+ name = config["external-editor"]
+ if not name:
+ return
+ self.save_msg()
+ tempname = tempfile.mktemp()
+ text = self.textwin.get('0.0', 'end')
+ helpers.write_text_to_named_file(tempname, text)
+ self.top.iconify()
+ exit_code, stdout, stderr = \
+ spawn.run(name, stdin='', args=[tempname],
+ env=os.environ)
+ self.top.deiconify()
+ if exit_code != 0 or stderr != '':
+ d = ErrorBox(self, "External editor failed",
+ "The external editor failed:\n" + stderr)
+ d.show()
+ os.remove(tempname)
+ return
+ text = helpers.read_text_from_named_file(tempname)
+ os.remove(tempname)
+ self.textwin.delete('0.0', 'end')
+ self.textwin.insert('0.0', text)
+ self.textwin.mark_set('insert', '0.0')
+
+ def save_msg(self):
+ txt = self.textwin.get('0.0', 'end')
+ txtfile = StringIO.StringIO(txt)
+ self.msg.change_text(txtfile)
+ self.feedback.refresh_draft_toc()
+
+ def send_msg(self):
+ d = YesNoDialog(self.top, "Send message?")
+ if d.show():
+ self.save_msg()
+ if self.feedback.send_msg(self.msg):
+ self.terminate()
+
+ def postpone_msg(self):
+ self.save_msg()
+ self.terminate()
+ d = InfoBox(self.top, "Message postponed",
+ "Message has been postponed.")
+ d.show()
+
+ def abort_msg(self):
+ d = YesNoDialog(self.top, "Abort message?")
+ if d.show():
+ self.msg.delete_self()
+ self.save_msg()
+ self.terminate()
+
+ def deleteWindow(self):
+ self.abort_msg()
+
+ def show(self):
+ Shells.NonModal.show(self)
+ self.textwin.focus_set()
diff --git a/slime-0.11/ui_config.py b/slime-0.11/ui_config.py
new file mode 100644
index 0000000..f184797
--- /dev/null
+++ b/slime-0.11/ui_config.py
@@ -0,0 +1,103 @@
+import os
+from Config import *
+from ConfigEditor import *
+
+config_file = "~/.slime/config"
+
+config_vars = {
+
+"count-width": (CONFIG_INTEGER, 3, "Width for message count in folder list"),
+
+"from-address": (CONFIG_STRING, "", "User's address (automatic, if empty)"),
+"reply-to": (CONFIG_STRING, "", "Default Reply-To"),
+"fcc-folder": (CONFIG_STRING, "",
+ "Folder to store copy of sent mail (no copy, if empty)"),
+"signature-file": (CONFIG_STRING, "~/.signature", "Signature file"),
+
+"make-pgp-signatures": (CONFIG_BOOLEAN, 0, "Make PGP signatures?"),
+"check-pgp-signatures": (CONFIG_BOOLEAN, 0, "Check PGP signatures?"),
+"pgp-path": (CONFIG_STRING, "/usr/bin/pgp", "PGP command (full path)"),
+"pgp-username": (CONFIG_STRING, "", "PGP username"),
+"pgp-max-password-age": (CONFIG_INTEGER, 60,
+ "Time (in minutes) to remember PGP passphrase"),
+
+"smtp-server": (CONFIG_STRING, "localhost", "SMTP server"),
+"max-references-length": (CONFIG_INTEGER, 500,
+ "Max length for References header"),
+"external-editor": (CONFIG_STRING, "", "External editor"),
+
+"normal-font": (CONFIG_STRING, "-misc-fixed-medium-r-semicondensed--13-120-75-75-c-60-iso8859-1",
+ "Normal font"),
+"unread-font": (CONFIG_STRING, "-misc-fixed-bold-r-semicondensed--13-120-75-75-c-60-iso8859-1",
+ "Unread font"),
+"command-font": (CONFIG_STRING, "-misc-fixed-medium-r-normal--10-*-*-*-*-*-iso8859-1",
+ "Command button font"),
+"max-subject-indent": (CONFIG_INTEGER, 4,
+ "Max subject indentation"),
+"max-folder-indent": (CONFIG_INTEGER, 2,
+ "Max folder indentation"),
+"important-headers": (CONFIG_STRING, "From To Cc Date Subject",
+ "Headers shown normally"),
+
+"inbox-scan-frequency": (CONFIG_INTEGER, 1,
+ "Frequency of new message scan (minutes)"),
+
+}
+
+config_layout = [
+ ["Mail sending",
+ "from-address",
+ "reply-to",
+ "smtp-server",
+ "max-references-length",
+ "fcc-folder",
+ "external-editor",
+ "signature-file",
+ ],
+ ["PGP",
+ "make-pgp-signatures",
+ "check-pgp-signatures",
+ "pgp-path",
+ "pgp-username",
+ "pgp-max-password-age",
+ ],
+ ["Appearance",
+ "normal-font",
+ "unread-font",
+ "max-subject-indent",
+ "max-folder-indent",
+ "important-headers",
+ "count-width",
+ ],
+ ["Misc",
+ "inbox-scan-frequency",
+ ],
+]
+
+config = Config(config_vars)
+
+def edit_config():
+ global config, config_layout
+ ConfigEditor(config, config_layout)
+
+def load_config():
+ p = os.path.expanduser(config_file)
+ if os.path.isfile(p):
+ f = open(p, "r")
+ config.read(f)
+ f.close()
+
+def save_config():
+ p = os.path.expanduser(config_file)
+ if os.path.isfile(p):
+ pnew = p + ".new"
+ fp = open(p, "r")
+ fpnew = open(pnew, "w")
+ config.rewrite(fp, fpnew)
+ fp.close()
+ fpnew.close()
+ os.rename(p + ".new", p)
+ else:
+ f = open(p, "w")
+ config.write(f)
+ f.close()
diff --git a/slime-0.11/ui_helpers.py b/slime-0.11/ui_helpers.py
new file mode 100644
index 0000000..ad52f94
--- /dev/null
+++ b/slime-0.11/ui_helpers.py
@@ -0,0 +1,155 @@
+from Tkinter import *
+import Shells, ButtonBox
+from ui_config import config
+
+class CommandButton(Button):
+ def __init__(self, master, text, command):
+ Button.__init__(self, master=master, text=text,
+ font=config["command-font"], padx=1, pady=0,
+ command=command)
+ self.pack(side=LEFT)
+
+class ModalDialog(Shells.Modal):
+ def terminate(self):
+ Shells.Modal.terminate(self)
+ self.top.update()
+
+ def show(self):
+ apply(Shells.NonModal.show, (self,))
+ self.initFocus()
+ self.top.waitvar(self.waitVar)
+ return self.result
+
+ def initFocus(self):
+ pass
+
+class PgpUsernameDialog(ModalDialog):
+ def __init__(self, infrontof, default_value):
+ ModalDialog.__init__(self, infrontof)
+ self.top.title("Enter PGP username")
+ self.top.iconname("Enter PGP username")
+ Label(self.top, text="Please enter the PGP username").pack()
+ self.entry = Entry(self.top, width=40)
+ self.entry.pack()
+ self.entry.bind("<Key-Return>", self.ok)
+ self.entry.bind("<Key-Escape>", self.cancel)
+ b = ButtonBox.T(self.top,
+ ("OK", self.ok), ("Cancel", self.cancel))
+ self.default_value = default_value
+
+ def ok(self, event=None):
+ self.terminate()
+ self.result = self.entry.get()
+
+ def cancel(self, event=None):
+ self.terminate()
+ self.result = None
+
+ def initControls(self):
+ self.entry.delete('0', 'end')
+ self.entry.insert('0', self.default_value)
+
+ def initFocus(self):
+ self.entry.focus_set()
+
+class PasswordDialog(ModalDialog):
+ def __init__(self, infrontof):
+ ModalDialog.__init__(self, infrontof)
+ self.top.title("Enter PGP Passphrase")
+ self.top.iconname("Enter PGP Passphrase")
+ Label(self.top, text="Please enter the PGP passphrase").pack()
+ self.entry = Entry(self.top, show="*", width=40)
+ self.entry.pack()
+ self.entry.bind("<Key-Return>", self.ok)
+ self.entry.bind("<Key-Escape>", self.cancel)
+ b = ButtonBox.T(self.top,
+ ("OK", self.ok), ("Cancel", self.cancel))
+
+ def ok(self, event=None):
+ self.terminate()
+ self.result = self.entry.get()
+
+ def cancel(self, event=None):
+ self.terminate()
+ self.result = None
+
+ def initControls(self):
+ self.entry.delete('0', 'end')
+
+ def initFocus(self):
+ self.entry.focus_set()
+
+class YesNoDialog(ModalDialog):
+ def __init__(self, infrontof, question, title=None):
+ ModalDialog.__init__(self, infrontof)
+ if not title:
+ title = question
+ self.top.title(title)
+ self.top.iconname(title)
+ Label(self.top, text=question).pack()
+ b = ButtonBox.T(self.top,
+ ("OK", self.ok), ("Cancel", self.cancel))
+
+ def ok(self):
+ self.result = 1
+ self.terminate()
+
+ def cancel(self):
+ self.result = 0
+ self.terminate()
+
+class NonModalDialog(Shells.NonModal):
+ def terminate(self):
+ Shells.NonModal.terminate(self)
+ self.top.update()
+
+class InfoBox(NonModalDialog):
+ def __init__(self, infrontof, title, text):
+ NonModalDialog.__init__(self, infrontof)
+ self.top.title(title)
+ self.top.iconname(title)
+ Message(self.top, text=text, width=250).pack()
+ Button(self.top, text="Dismiss", command=self.terminate).pack()
+
+class ErrorBox(InfoBox):
+ def __init__(self, infrontof, title, text):
+ InfoBox.__init__(self, infrontof, "Error: " + title, text)
+
+class FolderInfoEditor(ModalDialog):
+ def __init__(self, infrontof, foldername, is_inbox_now):
+ ModalDialog.__init__(self, infrontof)
+ self.top.title("Folder settings")
+ self.top.iconname("Folder settings")
+
+ Label(self.top, text="Settings for folder %s" % foldername).pack()
+
+ frame = Frame(self.top)
+ frame.pack(fill=X)
+ Label(frame, text="Scan for new messages?").pack(side=LEFT)
+ self.var = BooleanVar()
+ self.var.set(is_inbox_now)
+ self.is_inbox = Checkbutton(frame, variable=self.var,
+ command=self.setOnOffText)
+ self.is_inbox.pack(side=LEFT)
+
+ frame = Frame(self.top)
+ frame.pack(fill=X)
+ Button(frame, text="OK", command=self.ok).pack(side=LEFT)
+ Button(frame, text="Cancel", command=self.cancel).pack(side=LEFT)
+
+ self.setOnOffText()
+
+ def setOnOffText(self):
+ if self.var.get() == 0:
+ text = "No"
+ else:
+ text = "Yes"
+ self.is_inbox.config(text=text)
+
+ def ok(self):
+ self.result = self.var.get()
+ self.terminate()
+
+ def cancel(self):
+ self.result = None
+ self.terminate()
diff --git a/slime-main.jpg b/slime-main.jpg
new file mode 100644
index 0000000..f26f9d1
--- /dev/null
+++ b/slime-main.jpg
Binary files differ
diff --git a/slime-manual.html/ch1.html b/slime-manual.html/ch1.html
new file mode 100644
index 0000000..80fc084
--- /dev/null
+++ b/slime-manual.html/ch1.html
@@ -0,0 +1,22 @@
+<html><head>
+<title>Stupid Little Mailer: User's Guide - Introduction
+</title>
+<link rev=made href="mailto:liw@iki.fi">
+</head><body>
+<h1>
+Stupid Little Mailer: User's Guide - chapter 1<br>
+Introduction
+
+</h1>
+This manual will be written later.
+<hr>
+Stupid Little Mailer: User's Guide
+- <A href="index.html#copyright">Copyright © 1997 Lars Wirzenius.
+</A>
+<br>
+<A href="index.html#toc">Contents</A>; <A href="index.html#abstract">abstract</A>.
+<br>
+<address>Version very-pre-alpha
+<br>
+Lars Wirzenius <A href="mailto:liw@iki.fi">liw@iki.fi</A></address>
+</body></html> \ No newline at end of file
diff --git a/slime-manual.html/index.html b/slime-manual.html/index.html
new file mode 100644
index 0000000..f6ce121
--- /dev/null
+++ b/slime-manual.html/index.html
@@ -0,0 +1,51 @@
+<html><head>
+<title>Stupid Little Mailer: User's Guide</title>
+<link rev=made href="mailto:liw@iki.fi">
+</head><body>
+<h1>Stupid Little Mailer: User's Guide</h1>
+
+<A name="abstract"><h2>
+0.1 Abstract
+</h2></A>
+
+Stupid Little Mailer, or Slime for short, is a mail user
+agent.
+
+<hr>
+<A name="toc"><h2>
+0.2 Table of contents
+</h2></A>
+<ul>
+<li><A href="ch1.html">1 Introduction
+</A>
+</ul>
+<hr><A name="copyright"><h2>0.3 Copyright</h2></A>
+Copyright © 1997 Lars Wirzenius.
+<P>
+Slime, including this manual, is free software;
+you may redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free
+Software Foundation; either version 2, or (at your option)
+any later version.
+<P>
+This is distributed in the hope that it will be useful,
+but without any warranty; without even the implied warranty of
+merchantability or fitness for a particular purpose. See the
+GNU General Public License for more details.
+<P>
+You should have received a copy of the GNU General
+Public License with your Debian GNU/Linux system, in
+/usr/doc/copyright/GPL, or with the SeX source
+package as the file COPYING. If not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+<hr>
+Stupid Little Mailer: User's Guide
+- <A href="#copyright">Copyright © 1997 Lars Wirzenius.
+</A>
+<br>
+<A href="#toc">Contents</A>; <A href="#abstract">abstract</A>.
+<br>
+<address>Version very-pre-alpha
+<br>
+Lars Wirzenius <A href="mailto:liw@iki.fi">liw@iki.fi</A></address>
+</body></html> \ No newline at end of file
diff --git a/slime-msg.jpg b/slime-msg.jpg
new file mode 100644
index 0000000..dc1ff19
--- /dev/null
+++ b/slime-msg.jpg
Binary files differ
diff --git a/uitools/Alerts.py b/uitools/Alerts.py
new file mode 100644
index 0000000..ef8c8e4
--- /dev/null
+++ b/uitools/Alerts.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides some standard Alert dialogs.
+# This is a blatant imitation -- er, flattery -- of the tkdialogs
+# package, with extensions.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: Alerts.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import Shells, StdDialog, ProgressBar
+
+######################################################################
+# This is yer basic Alert type.
+######################################################################
+class T(Shells.Modal):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master, title="Alert", msg="[your message here]"):
+ Shells.Modal.__init__(self, master)
+ self.controls = StdDialog.Controls(self)
+ self.msg = msg
+ self.top.title(title)
+ self.justify = "center"
+
+ frame = self.frame = self.controls.frame
+
+ bm = self.bitmap = Tk.Label(frame)
+ bm.pack(side='left')
+
+ l = self.msgLabel = Tk.Label(frame, text=msg)
+ l.pack(side='top', fill='x', expand='yes')
+
+ self.btnBox = self.controls.btnBox
+
+ # For easier override/extend in subclasses
+ show = Shells.Modal.show
+
+ ##################################################################
+ # Standard callbacks for the various types of buttons.
+ ##################################################################
+ def okCB(self, event=None):
+ self.result = 1
+ self.terminate()
+
+ def cancelCB(self, event=None):
+ self.result = 0
+ self.terminate()
+
+
+######################################################################
+# And now for some common alerts:
+######################################################################
+
+######################################################################
+# Display an error message.
+######################################################################
+class Error(T):
+ def __init__(self, master, title="Error", msg="Error"):
+ T.__init__(self, master, title, msg)
+ self.bitmap['bitmap'] = 'error'
+ self.btnBox.addButtons(["OK", self.okCB])
+
+ # For easier override/extend in subclasses
+ show = T.show
+
+
+######################################################################
+# Display a warning.
+######################################################################
+class Warning(T):
+ def __init__(self, master, title="Warning", msg="Warning",
+ okLabel="Yes", cancelLabel="No"):
+ T.__init__(self, master, title, msg)
+ self.bitmap['bitmap'] = 'warning'
+ self.btnBox.addButtons([okLabel, self.okCB],
+ [cancelLabel, self.cancelCB])
+
+ # For easier override/extend in subclasses
+ show = T.show
+
+
+######################################################################
+# Display a question.
+######################################################################
+class Question(T):
+ def __init__(self, master, title="Question", msg="?",
+ okLabel="Yes", cancelLabel="No"):
+ T.__init__(self, master, title, msg)
+ self.bitmap['bitmap'] = 'question'
+ self.btnBox.addButtons([okLabel, self.okCB],
+ [cancelLabel, self.cancelCB])
+
+ # For easier override/extend in subclasses
+ show = T.show
+
+
+######################################################################
+# Display some information.
+######################################################################
+class Info(T):
+ def __init__(self, master, title="Info", msg="I",
+ okLabel="OK"):
+ T.__init__(self, master, title, msg)
+ self.bitmap['bitmap'] = 'info'
+ self.btnBox.addButtons([okLabel, self.okCB])
+
+ # For easier override/extend in subclasses
+ show = T.show
+
+
+######################################################################
+# Display instructions for the user.
+######################################################################
+class Instruction(T):
+ def __init__(self, master, title="Instruction", msg="",
+ okLabel="Continue", cancelLabel="Cancel"):
+ T.__init__(self, master, title, msg)
+ self.bitmap.destroy()
+ self.btnBox.addButtons([okLabel, self.okCB],
+ [cancelLabel, self.cancelCB])
+
+ # For easier override/extend in subclasses
+ show = T.show
+
+
+######################################################################
+# A progress dialog shows progress, i.e. percent-completion of a task.
+######################################################################
+class Progress(T):
+ def __init__(self, master, title="Progress", msg="",
+ cancelLabel="Cancel"):
+ T.__init__(self, master, title, msg)
+ self.bitmap.destroy()
+ self.progressBar = ProgressBar.T(self.frame)
+ self.btnBox.addButtons([cancelLabel, self.cancelCB])
+ self.progressBar.frame.pack(fill='x')
+
+ # For easier overriding/extension in subclasses:
+ show = T.show
+
+ ##################################################################
+ # Update the progress slider.
+ # percentComplete should be in the range 0..100, inclusive.
+ ##################################################################
+ def update(self, percentComplete):
+ self.progressBar.update(percentComplete)
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ f = Shells.Main()
+ l = Tk.Label(f, text="I'm just here for looks,\nreally.")
+ l.pack()
+ f.pack()
+ f.master.geometry("+100+100")
+ e = Error(f, msg="An unrecoverable error\nhas occurred.")
+ print "Error:", e.show()
+ w = Warning(f, msg="Since that error occurred, do you want to go on?")
+ print "Warning:", w.show()
+ print "Question:", Question(f, msg="No, I mean\ndo you really want\n"
+ "to go on?").show()
+ print "Info:", Info(f, msg="This parrot is dead.").show()
+ print "Instruction:", Instruction(f,
+ msg='Insert a blank tape and press '
+ '"Continue".').show()
+
+ class ProgressTest(Progress):
+ def __init__(self, master, title="Progress", msg=""):
+ Progress.__init__(self, master, title, msg)
+ self.value = 0
+
+ def show(self):
+ # Start the timer before showing, because Progress.show
+ # won't return until after the user has dismissed the dialog.
+ self.timerID = self.top.after(500, self.update)
+ result = Progress.show(self)
+ self.top.after_cancel(self.timerID)
+ return result
+
+ def update(self, event=None):
+ self.value = self.value + 1
+ Progress.update(self, self.value)
+ if self.value < 100:
+ self.timerID = self.top.after(40, self.update)
+ else:
+ self.result = 1
+ self.terminate()
+
+
+ print "Progress:", ProgressTest(f,
+ msg="Reading Enc. Britannica").show()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/ButtonBox.py b/uitools/ButtonBox.py
new file mode 100644
index 0000000..456b3d5
--- /dev/null
+++ b/uitools/ButtonBox.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides a composite widget for dialog buttons.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: ButtonBox.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import KWDict
+
+######################################################################
+# This class provides a horizontal array of std dialog buttons.
+######################################################################
+class T:
+ ##################################################################
+ # Add buttons to self.
+ # argtuple should be a tuple of lists or tuples
+ # specifying the label and (optionally) command for each button
+ # to be added to the frame.
+ ##################################################################
+ def _addButtons(self, argtuple):
+ Button = Tk.Button
+ f = self.frame
+ maxWidth = self.maxWidth
+ for btnInfo in argtuple:
+ label, cmd = btnInfo[0], None
+ if len(btnInfo) > 1:
+ cmd = btnInfo[1]
+ btn = Button(f, KWDict.dict(text=label, command=cmd))
+ btn.pack(side='left', padx='2m', expand='yes')
+ self.buttons[label] = btn
+ maxWidth = max(maxWidth, len(label))
+
+ self.maxWidth = maxWidth
+ for button in self.buttons.values():
+ button['width'] = maxWidth
+
+ ##################################################################
+ # Initialize a new instance.
+ # The optional arguments should each be lists or tuples
+ # specifying the label and (optionally) command for each button
+ # to be added to the frame.
+ ##################################################################
+ def __init__(self, master, *args):
+ self.master = master
+ f = self.frame = Tk.Frame(master, relief='raised', bd=1)
+
+ self.buttons = {}
+ self.maxWidth = 6
+ self._addButtons(args)
+ Tk.Pack.config(f, side='bottom', fill='x')
+
+ ##################################################################
+ # Add buttons to self.
+ # The optional arguments should each be lists or tuples
+ # specifying the label and (optionally) command for each button
+ # to be added to the frame.
+ ##################################################################
+ def addButtons(self, *args):
+ self._addButtons(args)
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ return
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/Cursors.py b/uitools/Cursors.py
new file mode 100644
index 0000000..647b81c
--- /dev/null
+++ b/uitools/Cursors.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+######################################################################
+# This module defines commonly-used CDE cursors.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: Cursors.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter
+
+# If you're running Solaris >= 2.4, you might want to try these
+# cursors:
+#busyCursorTuple = ('@/usr/dt/include/bitmaps/xm_hour16',
+# '/usr/dt/include/bitmaps/xm_hour16m',
+# 'black', 'white')
+#noEnterCursorTuple = ('@/usr/dt/include/bitmaps/xm_noenter16',
+# '/usr/dt/include/bitmaps/xm_noenter16m',
+# 'black', 'white')
+#
+# Clients can specify any of these cursors for CDE-style cursoring.
+#busyCursor = "%s %s %s %s" % busyCursorTuple
+#noEnterCursor = "%s %s %s %s" % noEnterCursorTuple
+
+busyCursor = "watch"
+noEnterCursor = "X_cursor red white"
+
+# _instances records all instances of Mixin. (See below.)
+_instances = {}
+
+######################################################################
+# This mixin class gives the ability to push and pop cursors on a
+# widget.
+######################################################################
+class Mixin:
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self):
+ # Register self to participate in cursoring. Whenever other
+ # Mixin instances pushOtherCursors(), self will get a new
+ # cursor.
+ _instances[self] = self
+ self.cursorStack = []
+
+ ##################################################################
+ # Destroy self. (Is this the right way to do this?)
+ ##################################################################
+ def destroy(self):
+ del _instances[self] # De-register self
+
+ ##################################################################
+ # Set a cursor on self.
+ ##################################################################
+ def setCursor(self, cursor):
+ # Why not just self.config(cursor=cursor)?
+ # I originally wanted to be able to mix this in to class Tk.
+ apply(self.tk.call, (self._w, "configure", "-cursor", cursor))
+
+ ##################################################################
+ # Push a cursor on self. (Doesn't affect other CursorMixin
+ # instances.)
+ ##################################################################
+ def pushCursor(self, newCursor):
+ self.setCursor(newCursor)
+ self.cursorStack.append(newCursor)
+
+ ##################################################################
+ # Pop a cursor off self.
+ ##################################################################
+ def popCursor(self):
+ stack = self.cursorStack
+ # Pop.
+ if stack:
+ del stack[-1]
+ # Re-install the old cursor, or else the default cursor.
+ if stack:
+ self.setCursor(stack[-1])
+ else:
+ self.setCursor('')
+
+ ##################################################################
+ # Push a cursor on all other registered instances.
+ # This is useful e.g. when displaying a modal dialog, and
+ # displaying busy cursors in other dialogs and toplevels.
+ ##################################################################
+ def pushOtherCursors(self, newCursor):
+ for w in _instances.keys():
+ if w != self:
+ w.pushCursor(newCursor)
+
+ ##################################################################
+ # Pop a cursor from all other registered instances.
+ ##################################################################
+ def popOtherCursors(self):
+ for w in _instances.keys():
+ if w != self:
+ w.popCursor()
+
diff --git a/uitools/KWDict.py b/uitools/KWDict.py
new file mode 100644
index 0000000..3fddac0
--- /dev/null
+++ b/uitools/KWDict.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides a means of converting a keyword argument list
+# to a dictionary. Thank you, "Internet Programming With Python".
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: KWDict.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+######################################################################
+# Build a dictionary using keyword notation. Include only items
+# whose values are not None.
+######################################################################
+def dict(**kw):
+ result = {}
+ for k, v in kw.items():
+ if v != None:
+ result[k] = v
+ return result
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ return
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/LabelEntry.py b/uitools/LabelEntry.py
new file mode 100644
index 0000000..203427a
--- /dev/null
+++ b/uitools/LabelEntry.py
@@ -0,0 +1,154 @@
+######################################################################
+# This class provides an Entry with a label on its left side.
+# The text of the label can be set by means of the 'text'
+# configuration key.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: LabelEntry.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+
+######################################################################
+# A MultiColEntry is a label together with one or more entries.
+# Clients have direct access to the constituent widgets:
+# frame -- the frame containing the other widgets.
+# label -- the label appearing on the left side
+# entries -- The entries arrayed across from left to right
+######################################################################
+class MultiColEntry:
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, columns=2, entryClass=Tk.Entry):
+ f = self.frame = Tk.Frame(master)
+ l = self.label = Tk.Label(f, anchor='ne')
+ l.pack(side='left')
+ self.entries = []
+ for i in range(columns):
+ e = entryClass(f)
+ self.entries.append(e)
+ e.pack(side='left', fill='x', expand='yes')
+
+ ##################################################################
+ # Set the values of one or more entry fields.
+ # It's the caller's responsibility not to exceed the number of
+ # entries in self.
+ ##################################################################
+ def setValues(self, startIndex=0, *newValues):
+ i = startIndex
+ for v in newValues:
+ e = self.entries[i]
+ e.delete('0', 'end')
+ e.insert('end', v)
+
+
+######################################################################
+# A labelled entry is a MultiColEntry with only one entry column.
+# The instance variable "entry" provides a more-convenient way to
+# refer to the sole entry widget.
+######################################################################
+class LabelEntry(MultiColEntry):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, entryClass=Tk.Entry):
+ MultiColEntry.__init__(self, master, 1, entryClass)
+ self.entry = self.entries[0]
+
+ ##################################################################
+ # Set the whole value of an Entry.
+ ##################################################################
+ def setValue(self, newValue):
+ self.entry.delete(0, 'end')
+ self.entry.insert('end', newValue)
+
+ ##################################################################
+ # Delete LabelEntry text.
+ ##################################################################
+ def delete(self, first, last=None):
+ self.entry.delete(first, last)
+
+ ##################################################################
+ # Insert LabelEntry text.
+ ##################################################################
+ def insert(self, index, string):
+ self.entry.insert(index, string)
+
+######################################################################
+# A ROLabelEntry is read-only, so far as the user is concerned.
+######################################################################
+class ROLabelEntry(LabelEntry):
+ def __init__(self, master=None):
+ LabelEntry.__init__(self, master, Tk.Entry)
+ self.entry['state'] = 'disabled'
+
+ ##################################################################
+ # Delete text from the entry.
+ ##################################################################
+ def delete(self, first, last=None):
+ self.entry['state'] = 'normal'
+ self.entry.delete(first, last)
+ self.entry['state'] = 'disabled'
+
+ ##################################################################
+ # Insert text into the entry.
+ ##################################################################
+ def insert(self, index, string):
+ self.entry['state'] = 'normal'
+ self.entry.insert(index, string)
+ self.entry['state'] = 'disabled'
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ f = Tk.Frame()
+
+ le = LabelEntry(f)
+ le.label['text'] = "New Value:"
+ newValue = "This is the new value."
+ le.entry['width'] = len(newValue)
+ le.insert('end', newValue)
+
+ readOnly = ROLabelEntry(f)
+ readOnly.label['text'] = 'Label:'
+ readOnly.entry['relief'] = 'groove'
+ for w in [readOnly.label, readOnly.entry]:
+ w['width'] = 40
+
+ readOnly.insert('0', 'This is the value')
+
+ mc = MultiColEntry(f, 8)
+ mc.label['text'] = "Multi:"
+ map(lambda e: e.config(width=8), mc.entries)
+
+ for w in [le, readOnly, mc]:
+ w.label['width'] = 12
+ w.frame.pack(fill='x')
+
+ f.update()
+ f.pack()
+ f.wait_visibility()
+ f.after(2000, readOnly.insert, 'end', ' after 2 seconds')
+
+ ##################################################################
+ # Insert text in a multi-column widget.
+ ##################################################################
+ def insertMulti(mc):
+ map(lambda e: e.insert('end', 'This is ' + `e`), mc.entries)
+
+ mc.setValues(2, "This is entry 2.", "This is entry 3.", "This is entry 4.")
+
+ f.after(4000, insertMulti, mc)
+ f.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/Menu.py b/uitools/Menu.py
new file mode 100644
index 0000000..8b64615
--- /dev/null
+++ b/uitools/Menu.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python
+######################################################################
+# Provide classes to simplify creating menubars, pulldown menus, etc.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: Menu.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import KWDict, string
+
+######################################################################
+# Convert a string of the form "&Something" to a tuple:
+# ("Something", 0)
+# indicating that the first letter of "Something" should be
+# underlined. If no "&" occurs inside the string, the second tuple
+# item will be None.
+######################################################################
+def findUnderline(title):
+ ul = None
+ i = string.find(title, "&")
+ if i >= 0:
+ ul = i
+ title = title[:ul] + title[ul+1:]
+ return title, ul
+
+
+######################################################################
+# This class represents a menubar, w. default relief, etc.
+######################################################################
+class Menubar(Tk.Frame):
+ ##################################################################
+ # Create a new instance.
+ ##################################################################
+ def __init__(self, master=None, **kw):
+ opts = {'relief':'raised', 'bd':2}
+ for k,v in kw.items():
+ opts[k] = v
+ apply(Tk.Frame.__init__, (self, master), opts)
+ Tk.Pack.config(self, side='top', fill='x')
+
+
+######################################################################
+# This class represents a menu item in a Tk menu.
+######################################################################
+class Item:
+ ##################################################################
+ # Create a new instance.
+ # master is the pane to which to append the new item.
+ # title is the text which will appear inside the menu item. It
+ # also determines what sort of menu item will be created. See
+ # below for details.
+ # cmd is the optional callback for this menu item.
+ # items specifies any items to be displayed in cascaded menus.
+ ##################################################################
+ def __init__(self, master, title, cmd=None, *items):
+ self.master = master
+ self.title = title
+
+ if title[0] == "-":
+ master.add_separator()
+ self.index = master.index('last')
+ return
+
+ submenu = None
+ createFn = master.add_command
+ prefix = title[0:2]
+ if prefix == "c_":
+ createFn = master.add_checkbutton
+ elif prefix == "r_":
+ createFn = master.add_radiobutton
+ elif prefix == "m_":
+ createFn = master.add_cascade
+ submenu = Pane(master)
+ apply(submenu.addItems, items)
+ else:
+ title = "x_" + title
+
+ title, ul = findUnderline(title[2:])
+
+ # XXX no means is provided to associate a Tk variable with a
+ # radio button or a check button.
+ apply(createFn, (), KWDict.dict(label=title, command=cmd,
+ underline=ul, menu=submenu))
+ self.index = master.index('last')
+
+ ##################################################################
+ # Enable or disable an item.
+ ##################################################################
+ def enable(self, doEnable=1):
+ if doEnable:
+ newstate = 'normal'
+ else:
+ newstate = 'disabled'
+
+ try:
+ # NOTE: This won't work for tear-off entries, or for separators.
+ self.master.tk.call(self.master._w, 'entryconfigure', self.index,
+ '-state', newstate)
+ except Tk.TclError: pass
+
+######################################################################
+# This class represents a pulldown menu.
+######################################################################
+class Pulldown:
+ ##################################################################
+ # Create a new Pulldown menu.
+ # Each additional item should be a list consisting of menu
+ # item string and optional callback. Menu item strings are
+ # interpreted as indicated in parseMenuStr().
+ # Returns the menu button which posts the new menu.
+ ##################################################################
+ def __init__(self, master, title, *items, **kw):
+ self.master = master
+
+ packside='left'
+ if kw and 'side' in kw.keys():
+ packside=kw['side']
+
+ # Create the menu button.
+ title, ul = findUnderline(title)
+ btnKeywords = KWDict.dict(text=title, underline=ul)
+ btn = apply(Tk.Menubutton, (master,), btnKeywords)
+
+ # Create the menu pane and associate it with the button.
+ menu = btn.menu = Pane(btn)
+ btn['menu'] = menu
+ apply(menu.addItems, items)
+
+ btn.pack(side=packside, padx='2m')
+
+
+######################################################################
+# This class extends the Tkinter Menu class with convenience methods
+# for enabling/disabling menu items and for support of popup menus.
+######################################################################
+class Pane(Tk.Menu):
+ ##################################################################
+ # Init a new instance.
+ ##################################################################
+ def __init__(self, master=None, **kw):
+ apply(Tk.Menu.__init__, (self, master), kw)
+ self.items = []
+
+ ##################################################################
+ # Add items to a menu Pane.
+ ##################################################################
+ def addItems(self, *newItems):
+ # Create a menu item for each described item.
+ for item in newItems:
+ self.items.append(apply(Item, (self,) + tuple(item)))
+
+ ##################################################################
+ # Enable or disable a menu item.
+ # If enable is non-zero, then the menu item is enabled; otherwise
+ # it is disabled.
+ ##################################################################
+ def enable(self, index, enable=1):
+ self.items[index].enable(enable)
+
+ ##################################################################
+ # Pop up a menu pane --
+ # useful when Pane is being used as a popup menu.
+ ##################################################################
+ def tk_popup(self, event=None):
+ x, y = 0, 0
+ if event:
+ x, y = event.x_root, event.y_root
+ self.tk.call('tk_popup', self._w, x, y)
+
+
+######################################################################
+# Mainline for testing
+######################################################################
+def main():
+ import TkDlgWrapper; TkDlg=TkDlgWrapper
+
+ # To demonstrate, here's a simple file editor.
+ f = Tk.Frame()
+ m = Menubar(f)
+ t = Tk.Text(f)
+
+ # Here are the file types which the open/save dialogs will recognize.
+ filetypes = (("Python", ".py"), ("Text", ".txt"), ("All Files", "*"))
+ openFile = TkDlg.OpenFile(f, defaultextension=".py",
+ filetypes=filetypes)
+ saveFile = TkDlg.SaveFile(f, defaultextension=".py",
+ filetypes=filetypes)
+
+ # Here's an example of a popup menu. It should appear when
+ # you click mouse button 3 inside the text area.
+ textPopup = Pane(f, tearoff='0')
+ textPopup.addItems(["Cut"], ["Copy"], ["Paste"], ["Clear"])
+ # Initially, disable all of the items. Enable them when a file
+ # is actually opened.
+ for item in textPopup.items:
+ item.enable(0)
+ t.bind("<3>", textPopup.tk_popup)
+
+ def openAFile(openDlg = openFile, text=t, popup=textPopup):
+ filename = openDlg.show()
+ if filename:
+ text.delete('1.0', 'end')
+ inf = open(filename)
+ text.insert('end', inf.read())
+ inf.close()
+ text.winfo_toplevel().title("Menu Example:%s" % filename)
+ # Enable the popup menu items.
+ for item in popup.items:
+ item.enable(1)
+
+ def saveAFile(saveDlg = saveFile, text=t):
+ filename = saveDlg.show()
+ if filename:
+ outf = open(filename, "w")
+ outf.write(text.get('1.0', 'end'))
+ outf.close()
+ text.winfo_toplevel().title("Menu Example:%s" % filename)
+
+ # Keyboard menu traversal appears to be broken under Tk 4.2 Try using
+ # keyboard traversal in the tk4.2 library's demo subdirectory...
+ fileMenu = Pulldown(m, "&File",
+ ["&Open...", openAFile],
+ ["Save &As...", saveAFile],
+ ["----"],
+ ["E&xit", m.quit])
+
+ helpMenu = Pulldown(m, "&Help",
+ ["&About"],
+ side='right')
+
+ m.pack()
+ t.pack()
+ f.pack()
+ f.master.title("Menu Example")
+ f.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/ProgressBar.py b/uitools/ProgressBar.py
new file mode 100644
index 0000000..0098ead
--- /dev/null
+++ b/uitools/ProgressBar.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides a progress bar, analogous to the progress
+# sliders in Win 3.1 or the progress slider in Netscape Nav.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: ProgressBar.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+
+######################################################################
+# This class provides a progress slider embedded within a Tk frame.
+######################################################################
+class T:
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, height=25, fillColor="blue"):
+ # Put the sunken relief on the frame, not the canvas.
+ # This way, if some subclass decides to embed a window within
+ # the canvas, the window won't ever draw itself into the
+ # relief area.
+ self.master = master
+ self.frame = Tk.Frame(master, relief='sunken', bd=2)
+ self.canvas = Tk.Canvas(self.frame, height=height, bd=0,
+ highlightthickness=0)
+ self.scale = self.canvas.create_rectangle(-10, -10, 0, height,
+ fill=fillColor)
+ self.canvas.pack(side='top', fill='x', expand='no')
+
+ ##################################################################
+ # Specify the completion value for self.
+ # percentComplete must be in the range 0..100, inclusive.
+ ##################################################################
+ def update(self, percentComplete):
+ c = self.canvas
+ width, height = c.winfo_reqwidth(), c.winfo_reqheight()
+ c.coords(self.scale, -10, -10,
+ percentComplete * width / 100.0, height)
+ c.update_idletasks()
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ class Test:
+ def __init__(self):
+ self.progress = T()
+ self.fraction = 0
+ self.progress.frame.pack()
+ self.progress.frame.after(1000, self.update)
+
+ def update(self, event=None):
+ self.fraction = self.fraction + 1
+ self.progress.update(self.fraction)
+ self.progress.frame.after(30, self.update)
+
+ t = Test()
+ Tk.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/README b/uitools/README
new file mode 100644
index 0000000..61471d1
--- /dev/null
+++ b/uitools/README
@@ -0,0 +1,186 @@
+________________________________________________________________________
+About UITools
+
+ There's been talk recently in comp.lang.python of the need
+ for a set of "standard" composite widget classes to complement
+ Tkinter.py. Here's my attempt to get the ball rolling.
+
+ This library consists of several modules which implement various
+ Python composite widget ("metawidget") classes. Most of these
+ classes are derived from code which I've been using for the past
+ year or so, but which I haven't heretofore organized.
+
+ Most modules have a "principal export". That is, they define
+ one main class along with zero or more derived/supporting
+ classes. The principal export is usually a class named "T".
+ This implies that these modules should be imported using
+ "import <module>" rather than "from <module> import *".
+
+ Classes which represent dialogs do not include "Dialog" or "Dlg"
+ in the class names. For example, Alerts.py exports several
+ alert dialog classes having names like Error, Warning, Question,
+ etc; Shells.py exports classes Main, Toplevel, NonModal and
+ Modal.
+
+ I omitted the "Dialog" suffix on the grounds that it was
+ redundant. Please let me know if you think this was a mistake.
+
+ Most modules include their own unit-test code, exported as
+ function main(). If a module is imported as a program's main
+ module, its main() function is automatically invoked. The
+ main() functions serve not only as rudimentary unit-tests, but
+ also as examples of how to use the classes defined in their
+ containing modules. For example, Menu.main() fires up a
+ simple-minded text editor, equipped with both Pulldown and Popup
+ menus.
+
+ Here's hoping this stuff is useful. At the least, I hope it
+ catalyzes contributions of other Python-based composite widget
+ classes.
+
+
+ -- Mitch Chapman
+ mchapman@erinet.com
+ 1 December 1996
+
+________________________________________________________________________
+Constituent Modules
+
+
+Alerts.py
+ Provides various error/warning/info dialogs. This module is
+ inspired by the alerts.py module of the tkdialogs archive.
+
+ The principal exported class, T, is a generic alert dialog.
+ Exported subclasses include:
+ Error -- for displaying error messages
+ Warning -- for displaying warnings
+ Question -- yes/no question presentation
+ Info -- displays informational messages
+ Instruction -- asks the user to perform a task and
+ press "Continue"
+ Progress -- shows the progress of some operation
+
+ButtonBox.py
+ Provides a frame which packs buttons horizontally.
+ Buttons can be accessed from the .buttons member dictionary,
+ using the labels by which the buttons were initially created.
+
+ (Would a simple array of buttons be easier to use?)
+
+Cursors.py
+ Provides a means of controlling cursors on an application-wide
+ basis. (For an example of use, see Shells.py.)
+
+KWDict.py
+ Provides a way to construct dictionaries from keyword argument
+ lists. Many thanks to "Internet Programming With Python,"
+ whence this code was stolen.
+
+LabelEntry.py
+ Provides multi-column, single-column and read-only labelled
+ entries.
+
+Menu.py
+ Provides a convenient means of constructing pulldown and popup
+ menus. With a single method call you can construct an entire
+ pulldown menu, complete with cascaded submenus.
+
+ Exported classes include Menubar (basically just a Tk.Frame);
+ Item, representing a single item within a menu pane; Pulldown,
+ representing a Menubutton and its associated menu pane; and
+ Pane, which can serve as either the pane for a Pulldown or as a
+ Popup.
+
+ This module was inspired by, but is less capable than, the
+ menu-creation functions in XEmacs 19.14.
+
+ This module needs more work to properly support Checkbutton and
+ Radiobutton menu items. The current version makes it hard to
+ associate Tk variables with such menu items.
+
+ProgressBar.py
+ Provides a status bar for displaying task completion percentages.
+ For an example of the use of a progress bar in a dialog, see
+ Alerts.py.
+
+Scrolled.py
+ Provides scrollbar decorations, along with scrolled texts,
+ canvases and lists. Exports include T, a generic
+ scrolled-window composite class into which an arbitrary "view"
+ widget may be inserted; Text, a scrolled text window; Canvas, a
+ scrolled canvas window; and List, a scrolled listbox.
+
+ This module was written using the Tk packer, rather than the
+ Tk 4.1+ grid widget. (The 4.1 grid widget looked a little
+ unreliable to me, and apparently not many people have yet
+ installed Tk 4.2.)
+
+ The scrollbars extend only to the bottom-right edge of the
+ scrolled view, using a technique described in Welch's "Practical
+ Programming with Tcl/Tk". (Dang! Is that the right title?)
+
+ In this version, both vertical and horizontal scrollbars are
+ displayed fulltime. I haven't yet found a way to dynamically
+ map and unmap the scrollbars which doesn't introduce
+ "flickering". (The scrollbar will map and unmap two or three
+ times before settling into a visible or invisible state.) Your
+ help is kindly requested.
+
+
+Shells.py
+ Provides various top-level, non-modal and modal dialog shells,
+ together with application-wide cursor control. Exports include
+ Main, a subclass of Tk.Frame; Toplevel, derived from
+ Tk.Toplevel; and NonModal and Modal dialog classes.
+
+ Main and Toplevel are provided because they support
+ application-wide cursor management. That is, when a
+ Shells.Modal dialog is mapped, all existing Main, Toplevel and
+ NonModal windows display a "do not enter" cursor.
+
+ The dialog classes are similar to those provided by the
+ tkdialogs archive. They differ in that they can easily be
+ created and initialized without being displayed. So, for
+ example, if you have a non-modal dialog which shows logged
+ network traffic, you can create it as soon as your application
+ starts and display it only when the user requests to see it.
+
+StdDialog.py
+ Provides "standard" modal and non-modal dialogs, as well as
+ button boxes populated with OK, Cancel and Help buttons.
+
+ Exported classes include Controls, a composite containing a
+ frame and a ButtonBox.T, stacked vertically; StdControls, a
+ subclass of Controls which populates its ButtonBox.T with "OK",
+ "Cancel" and "Help" buttons; NonModal and Modal, two dialog
+ classes whose managed contents consist of StdControl instances.
+
+ This module has a really poor design. I was trying to build a
+ framework for "typical" dialog windows, which contain a set of
+ controls packed into a frame, below which is a row of
+ dialog buttons. Class Controls represents the "typical" dialog
+ contents; StdControls represents typical contents along with
+ typical dialog buttons; and NonModal and Modal are dialogs whose
+ innards are StdControls.
+
+ Please help me straighten this out.
+
+TkDlgWrapper.py
+ Provides wrappers around the standard dialogs included with
+ Tk 4.2. (NOTE: This module _requires_ Tk 4.2 or later.)
+
+ The most significant exported classes include ChooseColor, an
+ interactive color-selection dialog, and OpenFile and SaveFile,
+ file open/save dialogs which display directory contents using
+ folder and document icons.
+
+ I originally released this module in October 1996 as "TkDialogs.py",
+ not knowing that ftp.python.org already had a tkdialogs archive.
+ Both this module module and the containing archive (UITools)
+ now have names which bear no resemblance to "tkdialogs", so
+ ending any potential name conflicts. I hope.
+
+ The classes have been renamed to omit "Dlg" from the class
+ names, but are otherwise unchanged from the original release.
+
diff --git a/uitools/Scrolled.py b/uitools/Scrolled.py
new file mode 100644
index 0000000..e2c1a53
--- /dev/null
+++ b/uitools/Scrolled.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+######################################################################
+# Provides a generic window containing a frame and h/v scrollbars.
+# Subclasses pack the scrollable/scannable thing into the frame and
+# hook it up to the scrollbars by invoking setView.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: Scrolled.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import string, time
+
+######################################################################
+# This is the scrolled window class.
+######################################################################
+class T:
+ ##################################################################
+ # Specify the view to be scrolled by self.
+ ##################################################################
+ def _setView(self, newView):
+ self.view = newView
+ self.view.pack(in_=self.borderframe, fill='both', expand='yes')
+ self.vsPacking['before'] = self.view
+ # Attach the scrollbars. Note that scrollable must support
+ # these methods: xview, yview, xscrollcommand, yscrollcommand.
+ self.vscroll['command'] = self.view.yview
+ self.hscroll['command'] = self.view.xview
+ self.view['xscrollcommand'] = self.hscroll.set
+ self.view['yscrollcommand'] = self.vscroll.set
+ # Make sure the view is visible to the user
+ # Gotta explicitly use Tk.Misc.tkraise, because if the view
+ # is a canvas it defines its own tkraise -- whose purpose
+ # is to change the stacking order of canvas items.
+ Tk.Misc.tkraise(self.view, self.borderframe)
+
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, view=None):
+ self.master = master
+ frame = self.frame = Tk.Frame(master)
+
+ # The inner frame is provided because the Tk Text widget
+ # (<= Tk 4.1) wasn't smart enough to keep embedded windows from
+ # drawing over its border. This inner frame can take the place
+ # of the Text border in subclasses.
+ self.borderframe = Tk.Frame(frame, relief='sunken', bd=3,
+ highlightthickness=2)
+
+ # Create the scrollbars. Record their packing rules for later
+ # use in dynamically mapping/unmapping scrollbars.
+ self.vsPacking = {'side':'right', 'fill':'y'}
+ vs = self.vscroll = Tk.Scrollbar(frame, orient='vertical', width=12)
+
+ # The horizontal scrollbar goes into its own frame at the
+ # bottom. This offers the opportunity to stick a "spacer"
+ # frame to the right of the horizontal bar, so both scrollbars
+ # appear to stop at the edge of the contained view.
+ # This is stolen from "Practical Programming in Tcl and Tk," by
+ # Brent B. Welch.
+ padsize = self.padsize = (string.atoi(vs['width']) +
+ 2 *(string.atoi(vs['bd']) +
+ string.atoi(vs['highlightthickness'])))
+ hsFrame = self.hsFrame = Tk.Frame(frame)
+ # Here's the "spacer" frame.
+ hsPad = self.hsPad = Tk.Frame(hsFrame, width=padsize, height=padsize)
+
+ hs = self.hscroll = Tk.Scrollbar(hsFrame, orient='horizontal',
+ width=12)
+ hs.pack(side='bottom', fill='x')
+ hsPad.pack(side='right', before=self.hscroll)
+ # This time, the packing is for self.hsFrame
+ self.hsFramePacking = {'before':vs,
+ 'side':'bottom', 'fill':'x', 'expand':'no'}
+
+ apply(vs.pack, (), self.vsPacking)
+ self.borderframe.pack(fill='both', expand='yes')
+ apply(hsFrame.pack, (), self.hsFramePacking)
+
+ self.view = None
+ self.hsPacked = 1
+ self.vsPacked = 1
+
+ # Bad move: initializer invoking another method on self.
+ # At least _setView is named so as to indicate that subclasses
+ # should not override it directly...
+ if view:
+ self._setView(view)
+
+ ##################################################################
+ # Install a scrollable thingy as the view for self.
+ # Note that you can change views on the fly, though it's up to
+ # you to remove any old views before setting the new one.
+ # newView should be a widget. It must provide Tkinter.Text-style
+ # methods xview() and yview(), and have configurable
+ # xscrollcommand and yscrollcommand attributes.
+ ##################################################################
+ def setView(self, newView):
+ self._setView(newView)
+
+
+######################################################################
+# This is a scrolled text class.
+######################################################################
+class Text(T):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None):
+ self.text = Tk.Text(master, relief='flat')
+ T.__init__(self, master, self.text)
+
+######################################################################
+# This is a scrolled canvas class.
+######################################################################
+class Canvas(T):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None):
+ self.canvas = Tk.Canvas(master)
+ T.__init__(self, master, self.canvas)
+
+######################################################################
+# A List differs from other Scrolled types in that it has only a
+# vertical scrollbar. (This despite the fact that lists with long
+# entries could benefit from horizontal scrollbars...)
+######################################################################
+class List(T):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None):
+ self.listbox = Tk.Listbox(master)
+ T.__init__(self, master, self.listbox)
+ self.hsFrame.forget()
+ self.view['xscrollcommand'] = None
+ self.hscroll['command'] = None
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ if 0:
+ # Here's one way to build a custom scrolled window:
+ scroller = T(view=Tk.Text(relief='flat', wrap='none'))
+ scroller.frame.pack(fill='both', expand='yes')
+ scroller.frame.master.title("Second Scroller")
+ Tk.mainloop()
+ else:
+ f = Tk.Frame()
+ t = Text(f)
+ # Turn off text wrapping so you can see the scrollbars at work.
+ t.text.configure(width=32, height=5, wrap='none')
+ t.text.insert('end', "This is a text.")
+ t.frame.pack(fill='both', expand='yes')
+
+ c = Canvas(f)
+ c.canvas.configure(scrollregion="-100 -100 1000 1000")
+ c.canvas.create_text(0, 0, text="This is a canvas.", anchor="nw")
+ c.canvas.create_arc(300, 300, 400, 400, extent=270, fill='red')
+ c.canvas.create_oval(100, 100, 200, 200, fill='blue')
+ c.frame.pack(fill='both', expand='yes')
+
+ l = List(f)
+ l.listbox.insert('end', "This is a listbox.")
+ for i in range(1, 21):
+ l.listbox.insert('end', "%d Mississippi" % i)
+ l.frame.pack(fill='both', expand='yes')
+
+ f.master.title("Sample Scrolled Windows")
+ f.pack()
+ Tk.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/Shells.py b/uitools/Shells.py
new file mode 100644
index 0000000..623deea
--- /dev/null
+++ b/uitools/Shells.py
@@ -0,0 +1,315 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides base classes for various shell classes.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: Shells.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import Cursors
+
+######################################################################
+# Make widget a transient for master.
+# widget should be a Toplevel.
+######################################################################
+def makeTransientFor(widget, master):
+ shell = master.winfo_toplevel()
+ name = shell.group() or "."
+ widget.group(name)
+ widget.transient(widget._nametowidget(name))
+
+######################################################################
+# This is a Tk Frame which knows how to register itself for cursor
+# stacking. It is intended to serve as the main window of an
+# application.
+######################################################################
+class Main(Tk.Frame, Cursors.Mixin):
+ def __init__(self, master=None, **kw):
+ apply(Tk.Frame.__init__, (self, master), kw)
+ apply(Cursors.Mixin.__init__, (self,))
+
+ def destroy(self):
+ Tk.Frame.destroy(self)
+ Cursors.Mixin.destroy(self)
+
+
+######################################################################
+# This is a Tk Toplevel which knows how to register itself for cursor
+# stacking.
+######################################################################
+class Toplevel(Tk.Toplevel, Cursors.Mixin):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, **kw):
+ apply(Tk.Toplevel.__init__, (self, master), kw)
+ apply(Cursors.Mixin.__init__, (self,))
+
+ ##################################################################
+ # Destroy an instance.
+ ##################################################################
+ def destroy(self):
+ Tk.Toplevel.destroy(self)
+ Cursors.Mixin.destroy(self)
+
+
+######################################################################
+# This is a modal dialog class.
+######################################################################
+class NonModal:
+ ##################################################################
+ # Initialize a new instance. master should be a widget in front
+ # of which self must appear.
+ ##################################################################
+ def __init__(self, master):
+ self.master = master
+ self.top = Toplevel(self.master)
+ self.top.withdraw()
+ self.isShowing = 0
+ self.top.title(self.__class__.__name__)
+ self.waitVar = `self.top` + 'EndDialogVar'
+
+ # Indicate that the dialog has not been centered in front of
+ # its master before.
+ self.isCentered = 0
+
+ # Modal dialogs may need to block input from other application
+ # windows by setting a grab on them. oldGrabber is where the
+ # previous owner of the app grab (if any) is saved.
+ self.oldGrabber = None
+
+ # In case somebody manages to blow away self by underhanded
+ # means, stop waiting for waitVar.
+ self.top.protocol("WM_DELETE_WINDOW", self.deleteWindow)
+
+ # A dialog has a result of arbitrary type.
+ # The result should be considered valid after the dialog is
+ # dismissed.
+ # Modal dialogs can use this to return arbitrary
+ # descriptions of what the user did. For example, a file-
+ # selection dialog might return either the pathname specified
+ # by the user or, if the dialog was cancelled, None.
+ self.result = None
+
+ ##################################################################
+ # Default response to an attempt to delete the window: ignore it.
+ ##################################################################
+ def deleteWindow(self):
+ return
+
+ ##################################################################
+ # Show a dialog.
+ ##################################################################
+ def show(self):
+ if self.isShowing:
+ # Dialog is already visible. Raise it to the top.
+ self.top.tkraise()
+ return
+ # Give us a chance to initialize contents. Other program
+ # state may have changed while the dialog was unmapped.
+ self.initControls()
+
+ self.center()
+ # Remember the old grab owner, for modal subclasses.
+ self.oldGrabber = self.top.grab_current()
+
+ self.top.deiconify()
+
+ # Here's where modal subclasses get a chance to set a grab.
+ self.top.wait_visibility()
+ self.modalGrab()
+
+ self.top.focus()
+ # Old results are invalid, now.
+ self.result = None
+ self.isShowing = 1
+
+ ##################################################################
+ # Initialize the contents of the dialog, e.g. put default values
+ # into text fields.
+ ##################################################################
+ def initControls(self):
+ return
+
+ ##################################################################
+ # Center the dialog in front of its master, if possible.
+ ##################################################################
+ def center(self):
+ if self.isCentered:
+ # Center the dialog only the first time it is displayed.
+ # After that, just pop it up where the user left it.
+ return
+
+ # Try to ensure any geometry requests have been processed.
+ self.top.update_idletasks()
+
+ w = self.master.winfo_width()
+ h = self.master.winfo_height()
+ x = self.master.winfo_rootx()
+ y = self.master.winfo_rooty()
+ reqw = self.top.winfo_reqwidth()
+ reqh = self.top.winfo_reqheight()
+
+ centerx = `x + (w - reqw)/2`
+ centery = `y + (h - reqh)/2`
+ geomStr = "+" + centerx + "+" + centery
+ self.top.geometry(geomStr)
+ self.isCentered = 1
+
+ ##################################################################
+ # This is a hook for modal dialog subclasses. It gives them a
+ # chance to do a grab_set on the dialog's Toplevel, to prevent
+ # input from being delivered to other application windows.
+ ##################################################################
+ def modalGrab(self):
+ return
+
+ ##################################################################
+ # This is another hook for modal dialog subclasses. It allows
+ # for the release of any grab set via modalGrab.
+ ##################################################################
+ def modalReleaseGrab(self):
+ return
+
+ ##################################################################
+ # Terminate the dialog, returning its result.
+ ##################################################################
+ def terminate(self):
+ # Set the wait variable, so modal dialogs will know they're
+ # done.
+ self.top.setvar(self.waitVar, 1)
+ self.top.withdraw()
+ self.isShowing = 0
+
+ # Here's where modal subclasses get a chance to release any
+ # grab they may have taken.
+ self.modalReleaseGrab()
+
+ self.master.focus()
+ return self.result
+
+ ##################################################################
+ # Destroy the visuals.
+ ##################################################################
+ def destroy(self):
+ self.top.destroy()
+
+
+######################################################################
+# This class represents an application-modal dialog.
+######################################################################
+class Modal(NonModal):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master):
+ apply(NonModal.__init__, (self, master))
+
+ # Make self a transient for its master. This basically forces
+ # self to remain in front of its master, and not to be
+ # separately iconifiable. (Depends on the window manager?)
+ #
+ # Note this isn't done for non-modal dialogs. Sorry,
+ # OpenWindows fans, but I can't stand having to slide
+ # non-modal dialogs out of the way just so I can see the
+ # master window...
+ makeTransientFor(self.top, master)
+
+ # If somebody had an application-wide pointer grab,
+ # remember them here. When the dialog is dismissed, the
+ # grab will be returned to the previous owner.
+ self.oldGrabber = None
+
+ ##################################################################
+ # Show a modal dialog.
+ ##################################################################
+ def show(self):
+ apply(NonModal.show, (self,))
+ self.top.waitvar(self.waitVar)
+ return self.result
+
+ ##################################################################
+ # Put an application-wide grab into effect, to block input from
+ # other application windows.
+ ##################################################################
+ def modalGrab(self):
+ # Display a do-not-enter cursor in front of all other toplevels
+ # in the application.
+ self.top.pushOtherCursors(Cursors.noEnterCursor)
+ self.top.grab_set()
+
+ ##################################################################
+ # Release the grab obtained via modalGrab().
+ ##################################################################
+ def modalReleaseGrab(self):
+ self.top.grab_release()
+ # If somebody else owned the grab before we did, give it
+ # back to them.
+ if self.oldGrabber and (self.oldGrabber != self.top):
+ self.oldGrabber.grab_set()
+
+ self.top.popOtherCursors()
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ global f, nonmodal, modal
+
+ # Display a non-modal dialog.
+ f = Main()
+ nonmodal = None
+ modal = None
+
+ def showNonModal(event=None):
+ global f, nonmodal
+
+ if not nonmodal:
+ nonmodal = NonModal(f)
+ label = Tk.Label(nonmodal.top,
+ text="This is a non-modal dialog.")
+ label.pack()
+
+ def quitCB(event=None, dlg=nonmodal):
+ result = dlg.terminate()
+ print "Dialog terminated with result", `result`
+ btn = Tk.Button(nonmodal.top, text="OK", command=quitCB)
+ btn.pack()
+
+ nonmodal.show()
+
+ def showModal(event=None):
+ global f, modal
+
+ if not modal:
+ modal = Modal(f)
+ label = Tk.Label(modal.top, text="This is a modal dialog.")
+ label.pack()
+
+ def quitCB(event=None, dlg=modal):
+ result = dlg.terminate()
+ print "Dialog terminated with result", `result`
+ btn = Tk.Button(modal.top, text="OK", command=quitCB)
+ btn.pack()
+
+ modal.show()
+
+ for text, command in [("NonModal", showNonModal),
+ ("Modal", showModal),
+ ("Quit", f.quit)]:
+ b = Tk.Button(f, text=text, width=10, command=command)
+ b.pack()
+
+ f.pack()
+ f.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/StdDialog.py b/uitools/StdDialog.py
new file mode 100644
index 0000000..3e19e12
--- /dev/null
+++ b/uitools/StdDialog.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+######################################################################
+# This module provides some standard dialog classes.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: StdDialog.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import Shells, ButtonBox
+
+######################################################################
+# This class creates basic dialog contents: a frame and
+# a btnBox.
+######################################################################
+class Controls:
+ ##################################################################
+ # Init a new instance. master must be a Shells.NonModal
+ # subclass.
+ ##################################################################
+ def __init__(self, master, okCB=None, cancelCB=None, helpCB=None):
+ self.master = master
+
+ # Subclasses should put their custom controls inside the
+ # frame.
+ self.frame = Tk.Frame(master.top, relief='raised', bd=1)
+ self.frame.pack(side='top', fill='both', expand='yes')
+
+ self.btnBox = ButtonBox.T(master.top)
+ self.btnBox.frame.pack()
+
+
+######################################################################
+# This class creates "standard" dialog contents and some std buttons.
+######################################################################
+class StdControls(Controls):
+ ##################################################################
+ # Init a new instance. master must be a Shells.NonModal
+ # subclass.
+ ##################################################################
+ def __init__(self, master, okCB=None, cancelCB=None, helpCB=None):
+ apply(Controls.__init__, (self, master))
+
+ self.btnBox.addButtons(["OK", okCB],
+ ["Cancel", cancelCB],
+ ["Help", helpCB])
+
+######################################################################
+# This is a non-modal dialog with the standard dialog buttons.
+######################################################################
+class NonModal(Shells.NonModal):
+ ##################################################################
+ # Init a new instance.
+ # The optional arguments are lists or tuples, each specifying the
+ # name and (optionally) command for a new dialog button.
+ ##################################################################
+ def __init__(self, master, *args):
+ Shells.NonModal.__init__(self, master)
+ self.controls = StdControls(self, self.okCB, self.cancelCB,
+ self.helpCB)
+
+ ##################################################################
+ # Define callbacks for the standard buttons, for subclasses.
+ ##################################################################
+ def okCB(self, event=None):
+ self.terminate()
+ return self.result
+
+ def cancelCB(self, event=None):
+ self.terminate()
+ return self.result
+
+ def helpCB(self, event=None):
+ return
+
+######################################################################
+# This is a modal dialog with the standard set of dialog buttons
+# arrayed across the bottom.
+######################################################################
+class Modal(Shells.Modal):
+ ##################################################################
+ # Initialize a new instance.
+ # The optional arguments are lists or tuples, each specifying the
+ # name and (optionally) command for a new dialog button.
+ ##################################################################
+ def __init__(self, master, *args):
+ Shells.Modal.__init__(self, master)
+ self.controls = StdControls(self, self.okCB, self.cancelCB,
+ self.helpCB)
+
+ ##################################################################
+ # Define callbacks for the standard buttons, for subclasses.
+ ##################################################################
+ def okCB(self, event=None):
+ self.terminate()
+ return self.result
+
+ def cancelCB(self, event=None):
+ self.terminate()
+ return self.result
+
+ def helpCB(self, event=None):
+ return
+
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ f = Shells.Main()
+ f.pack()
+
+ quitBtn = Tk.Button(f, text="Quit", command=f.quit)
+ quitBtn.pack()
+
+ d = Modal(f)
+ Tk.Label(d.controls.frame, text="Press a button").pack()
+
+ result = d.show()
+ print "Modal Result =", result
+ d.destroy()
+ d = NonModal(f)
+ Tk.Label(d.controls.frame,
+ text="Press a button\nwhenever you're ready").pack()
+ d.show()
+ f.mainloop()
+
+if __name__ == "__main__":
+ main()
diff --git a/uitools/TkDlgWrapper.py b/uitools/TkDlgWrapper.py
new file mode 100644
index 0000000..54b2236
--- /dev/null
+++ b/uitools/TkDlgWrapper.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python
+######################################################################
+# Try to access some of the newer Tk dialogs.
+# This module is inspired by the Python 1.[34] Dialog.py module.
+#
+# Mitch Chapman
+#---------------------------------------------------------------------
+# $Log: TkDlgWrapper.py,v $
+# Revision 1.1 1996/12/01 22:58:54 mchapman
+# Initial revision
+#
+######################################################################
+
+__version__ = "$Revision: 1.1 $"
+
+import Tkinter; Tk=Tkinter
+import os, copy
+
+VersionError = "VersionError"
+
+if Tk.TkVersion < 4.2:
+ raise VersionError, "This module requires Tk 4.2 or greater."
+
+######################################################################
+# This is an abstract wrapper for all classes of built-in Tk dialogs.
+# It's subclassed from Tk.Widget to gain access to widget
+# configuration methods.
+######################################################################
+class T(Tk.Widget):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, widgetName, master=None, **kw):
+ self.widgetName = "__%s__" % widgetName
+ # The unadorned widgetName is the Tk command/proc which
+ # displays the dialog, e.g. tk_chooseColor
+ self.tkCommand = widgetName
+ self.master = master
+ # Defer the actual widget initialization. I want instances
+ # of T to persist even when they are not visible. For
+ # example, this allows a file selection dialog to remember
+ # what file extension it last selected. But in order to allow
+ # persistence, the creation of the underlying Tk dialog must
+ # be delayed.
+
+ # Make a copy of the configuration, so it can persist. This
+ # is where self remembers things such as the last file type
+ # selected, last color used, etc.
+ self.kw = copy.copy(kw)
+
+ ##################################################################
+ # Show the dialog.
+ ##################################################################
+ def show(self, **kw):
+ # Use the configuration options to override the current config --
+ # and remember them for later.
+ for k, v in kw.items():
+ self.kw[k] = v
+ Tk.Widget._setup(self, self.master, self.kw)
+ resultStr = apply(self.tk.call, (self.tkCommand,) +
+ self._options(self.kw))
+ try:
+ # Don't leak Python widget descriptors?
+ Tk.Widget.destroy(self)
+ except Tk.TclError: pass
+
+ return resultStr
+
+ ##################################################################
+ # Explicitly destroy a widget instance. Nothing to do, here,
+ # because the widget is gone as soon as show() returns.
+ ##################################################################
+ def destroy(self):
+ pass
+
+
+######################################################################
+# Abstract wrapper for the Tk 4.2 tk_get\(Open\|Save\)File dialog.
+# Tries to remember the last-used directory, so it can be used as
+# the initial directory next time the dialog is displayed.
+######################################################################
+class OpenSaveFile(T):
+ ##################################################################
+ # Initialize a new instance. whichDlg tells whether this is
+ # a tk_getOpenFile or a tk_getSaveFile, etc.
+ ##################################################################
+ def __init__(self, whichDlg, master=None, **kw):
+ apply(T.__init__, (self, whichDlg, master), kw)
+ self.filename = None
+
+ ##################################################################
+ # Show the dialog and return either the selected file or "".
+ ##################################################################
+ def show(self, **kw):
+ self.filename = apply(T.show, (self,), kw)
+ # Try to remember a few configuration items for the next time
+ # the dialog is displayed.
+ if self.filename:
+ dirname, basename = os.path.split(self.filename)
+
+ # Next time, start in the directory we ended in this time.
+ self.kw['initialdir'] = dirname
+
+ # Try to figure out what extension to use next time.
+ ext = os.path.splitext(basename)[1]
+ self.kw['defaultextension'] = "%s" % ext
+
+ # Try to specify the default name to use next time.
+ self.kw['initialfile'] = basename
+
+ # The Tk dialogs are a little stupid: Even if you specify
+ # an initial file with extension ".txt", the dialog will show
+ # only files matching the first set of extensions in the
+ # -filetypes option.
+ # My cheap workaround is to create a new first entry for
+ # -filetypes which matches the extension selected this
+ # time.
+ oldtypes = ()
+ if self.kw.has_key('filetypes'):
+ oldtypes = self.kw['filetypes']
+ # The following works only for Unix (and maybe Windows).
+ # Somebody wanna fix this for handling Mac file types?
+ newtypes = ()
+ dfltExt = ext or "*"
+ dfltDescription = "(Last Selected)"
+ for typespec in oldtypes:
+ description, types = typespec
+ if description == dfltDescription:
+ newtypes = ((dfltDescription, dfltExt),) + oldtypes[1:]
+ break # EXIT FOR LOOP
+ else:
+ newtypes = ((dfltDescription, dfltExt),) + oldtypes
+
+ self.kw['filetypes'] = newtypes
+
+ # The Tk dialogs are a little smart: If you create both an
+ # open and a save dialog in the same application, they seem
+ # to both slave to the same working directory. Change to dir
+ # "/blah" when opening? Then when you try to save, the
+ # default displayed directory will be "/blah".
+ # This may be a bug, given what the man pages say, but
+ # I like it.
+ return self.filename
+
+######################################################################
+# Wrapper for the tk_getOpenFile dialog.
+######################################################################
+class OpenFile(OpenSaveFile):
+ def __init__(self, master=None, **kw):
+ apply(OpenSaveFile.__init__, (self, "tk_getOpenFile", master), kw)
+
+
+######################################################################
+# Wrapper for the tk_getSaveFile dialog.
+######################################################################
+class SaveFile(OpenSaveFile):
+ def __init__(self, master=None, **kw):
+ apply(OpenSaveFile.__init__, (self, "tk_getSaveFile", master), kw)
+
+
+######################################################################
+# Wrapper for the tk_chooseColor dialog.
+######################################################################
+class ChooseColor(T):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, **kw):
+ apply(T.__init__, (self, "tk_chooseColor", master), kw)
+
+
+######################################################################
+# Wrapper for the tk_messageBox dialog.
+######################################################################
+class MessageBox(T):
+ ##################################################################
+ # Initialize a new instance.
+ ##################################################################
+ def __init__(self, master=None, defaultCfg=None, **kw):
+ cfg = Tk._cnfmerge((defaultCfg or {}, kw))
+ apply(T.__init__, (self, "tk_messageBox", master), cfg)
+
+
+######################################################################
+# These are the specific types of MessageBox dialogs provided by Tk.
+# For each of these classes, the return value of show() is a string
+# containing the label of the button which was pressed.
+#
+# Yeah, I know, these aren't particularly useful when you can roll
+# your own much more beautiful dialogs in Python. But hey! They're
+# here and they work, and they're supposed to have native look-n-feel
+# on every supported platform.
+######################################################################
+
+class AbortRetryIgnore(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__,
+ (self, master, {'icon':'error', 'type':'abortretryignore'}), kw)
+
+class OK(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__, (self, master, {'type':'ok'}), kw)
+
+class OKCancel(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__, (self, master, {'type':'okcancel'}), kw)
+
+class RetryCancel(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__,
+ (self, master, {'icon':'error', 'type':'retrycancel'}), kw)
+
+class YesNo(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__,
+ (self, master, {'icon':'question', 'type':'yesno'}), kw)
+
+class YesNoCancel(MessageBox):
+ def __init__(self, master=None, **kw):
+ apply(MessageBox.__init__,
+ (self, master, {'icon':'question', 'type':'yesnocancel'}), kw)
+
+
+######################################################################
+# Don't wrap tk_dialog -- that's already done in Dialog.py.
+######################################################################
+
+######################################################################
+# Main function for unit testing.
+######################################################################
+def main():
+ # Demo all of the message box dialog classes:
+ for c in [AbortRetryIgnore, OK, OKCancel,
+ RetryCancel, YesNo, YesNoCancel]:
+ d = c(title=c.__name__)
+ print d.show(message="This is an instance of %s." % c.__name__)
+
+ # Demo the color chooser dialog:
+ d = ChooseColor(title="PickaKuhla", initialcolor="SlateGray")
+ color = d.show()
+ print "Selected color is", `color`
+
+ # Demo the open file dialog.
+ d = OpenFile(defaultextension=".py",
+ filetypes=(("Python", ".py"),
+ ("Emacs Lisp", ".el"),
+ ("Text", ".txt"),
+ ("All Files", "*")))
+
+ # Run multiple times to demonstrate how the dialog remembers the
+ # last working directory, file extension, etc.
+ for i in range(2):
+ filename = d.show()
+ print "Open file", `d.filename`
+
+ print 72 * "_"
+ print "Current working directory is", os.getcwd()
+ d = SaveFile(defaultextension=".py",
+ filetypes=(("Pithon [er, Python]", ".py"),
+ ("Text", ".txt"),
+ ("All Files", "*")))
+ for i in range(2):
+ filename = d.show()
+ print "Save File As", `d.filename`
+
+
+if __name__ == "__main__":
+ main()