lamson-1.0pre11/0000755000076500000240000000000011313464575013055 5ustar zedshawstafflamson-1.0pre11/bin/0000755000076500000240000000000011313464573013623 5ustar zedshawstafflamson-1.0pre11/bin/lamson0000755000076500000240000000022211200524130015013 0ustar zedshawstaff#!/usr/bin/env python from lamson import args, commands import sys args.parse_and_run_command(sys.argv[1:], commands, default_command="help") lamson-1.0pre11/build.vel0000644000076500000240000000547211313463463014667 0ustar zedshawstaff# Copyright (C) 2008 Zed A. Shaw. You're free to reuse this file # in your build scripts in anyway and remove the copyright # notice. imports [ recipe(from 'scripts/testing' as 'testing') recipe(from 'scripts/dist' as 'dist') recipe(from 'scripts/sample' as 'sample') ] options( project "lamson" default 'tests' sudo 'sudo' version '1.0pre11' website './doc/lamsonproject.org/output/releases/' version.file 'lamson/version.py' setup( name 'lamson' version '1.0pre11' author 'Zed A. Shaw' description 'Lamson is a modern Pythonic mail server built like a web application server.' url 'http://pypi.python.org/pypi/lamson' download_url 'http://pypi.python.org/pypi/lamson' author_email 'zedshaw@zedshaw.com' package_data ( lamson [ 'data/prototype.zip' ]) packages ['lamson' 'lamson.handlers'] scripts ['bin/lamson'] install_requires ['chardet' 'jinja2' 'mock' 'nose' 'python-daemon'] ) ) depends( build ['tests' 'prototype' 'examples' 'virus' 'version.gen' 'dist.install' 'dist.sdist'] commit ['dist.gen.setup' 'parser' 'dist.clean'] tests ['parser' 'testing.run'] release ['build' 'dist.release' 'book.release' ] clean ['dist.clean'] pypi ['build'] ) targets( commit [ $ bzr log --short > CHANGES $ bzr commit $ bzr push ] virus [ $ clamscan -i -r ] examples [ $ rm -rf run/queue examples/osb/run/* examples/librelist/run/* $ rm -rf examples/librelist/app/data/archive/* $ find . -name "main.db" -exec rm {} \; $ find . -name "*.log" -exec rm {} \; $ find . -name "*.err" -exec rm {} \; $ find . -name "*.out" -exec rm {} \; $ find . -name "*.pyc" -exec rm {} \; $ find . -name "*.sw*" -exec rm {} \; $ rm -f tests/test.db $ rm -f tests/sbdb $ rm -rf doc/lamsonproject.org/output/docs/api ] prototype [ $ rm -f lamson/data/prototype.zip $ cd lamson/data/prototype ; zip -r ../prototype.zip . ] version.gen [ $ fossil info | grep checkout > /tmp/lamson.rev py [ |rev = open("/tmp/lamson.rev").read().split()[1] |ver = {"version": version, "rev": [rev[:10], rev]} |open("%(version.file)s", 'w').write( | "VERSION=" + repr(ver)) ] ] coverage [ $ nosetests --quiet --with-coverage --cover-package lamson ] pypi [ $ python setup.py register sdist bdist_egg upload ] ) lamson-1.0pre11/doc/0000755000076500000240000000000011313464573013620 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/0000755000076500000240000000000011313464573017266 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/build.vel0000644000076500000240000000131211217111635021061 0ustar zedshawstaffimports [ module(from "vellum.commands") ] options( default 'sync' sudo 'sudo' ) depends( sync ['gen'] ) targets( commit $ bzr commit gen [ $ python webgen.py $ cd ../.. && epydoc --graph classtree --url http://lamsonproject.org/ --name "Lamson Mail Server" --html --redundant-details lamson -o doc/lamsonproject.org/output/docs/api/ ] sync [ $ rsync -av input/* output/ $ rsync -avuz output/* zedshaw@www.zedshaw.com:/usr/local/nginx/html/lamsonproject/ $ rsync -av ../../dist/* zedshaw@www.zedshaw.com:/usr/local/nginx/html/lamsonproject/releases/ ] ) lamson-1.0pre11/doc/lamsonproject.org/ChangeLog0000644000076500000240000000374711310773564021053 0ustar zedshawstaff=== 2009-12-12 === 20:00:38 [42768ade2b] *CURRENT* 1.0pre8 release announced and done. (user: zedshaw tags: trunk, 1.0pre8) 19:43:53 [bb1b0a0dae] Updated MailBase.__nonzero__ to include parts as well. (user: zedshaw tags: trunk) 19:40:51 [2916b83307] Created ChangeLog from fossil timeline of commits. (user: zedshaw tags: trunk) 19:36:14 [41530d8a34] Version bump to 1.0pre8 for release. (user: zedshaw tags: trunk) 19:31:21 [dfe158b282] Initial support of using Delivered-To or similar more exact To header. (user: zedshaw tags: trunk) 19:14:13 [e9988679b7] Updated prototype.zip for upcoming release. (user: zedshaw tags: trunk) 19:13:51 [98e94b6103] Potentially fixed the unicode/ascii issues with shelve storage of confirmations and routing. (user: zedshaw tags: trunk) 18:56:35 [6224ae68e4] Updated docs about confirmation to fix a doc bug. (user: zedshaw tags: trunk) 18:56:14 [103454c43c] Make things clean up better on INT or TERM (not HUP). (user: zedshaw tags: trunk) 18:25:29 [009451cabc] Added meta tags to the site so it can be found easier. I didn't know search engines still use that crap. (user: zedshaw tags: trunk) 18:22:24 [8908d7852d] Applied starttls, username, password patch to server. (user: zedshaw tags: trunk) 18:16:01 [9a9f9f37b6] Applied patch for handling odd email addresses and white- listing what headers have email addresses. (user: zedshaw tags: trunk) 18:06:06 [7605ef0517] Patch for queue bugs applied [bug 9d330c8f80] (user: zedshaw tags: trunk) 18:02:50 [ef9808c4f7] Point at the right archive browser. (user: zedshaw tags: trunk) 18:01:39 [3b3dc9391a] Better logging message on failure to connect to relay. (user: zedshaw tags: trunk) === 2009-12-05 === 18:05:40 [ddc4369b1d] Initial commit to the new fossil based support site. (user: zedshaw tags: trunk) 17:48:22 [8140fcad03] initial empty check-in (user: lamson tags: trunk) lamson-1.0pre11/doc/lamsonproject.org/config.py0000644000076500000240000000122711225636102021076 0ustar zedshawstaffimport os author = 'Zed A. Shaw' # Default author name. Overridden in individual document THIS_DIR = os.path.abspath(os.path.dirname(__file__)) input_dir = os.path.join(THIS_DIR, 'input') output_dir = os.path.join(THIS_DIR, 'output') template_dir = THIS_DIR template = os.path.join(template_dir, 'template.html') ### Optional parameters options = { 'baseurl':"", # if not set, relative URLs will be generated 'sitename':"Lamson Project(TM)", 'slogan': "Lamson The Python SMTP Server and Framework", 'extensions':['.txt'], 'format': 'text/x-textile', 'siteurl': 'http://lamsonproject.org', } lamson-1.0pre11/doc/lamsonproject.org/input/0000755000076500000240000000000011313464573020425 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/about.txt0000644000076500000240000000353711210043226022270 0ustar zedshawstaffFrom: Zed Title: About Lamson Lamson started more than a year ago as a fun side project of "mine":http://zedshaw.com/ after a few evil experiences with sendmail and various mailing list software. I realized that e-mail systems didn't have anything like a modern "framework" for building applications. Rather than replicate the mess of aliases, pipes, processes, and nasty m4 macros that currently existed, I decided to write something different. Lamson was originally called "Son Of Sam", but that name proved too difficult to work with as a project name. Apparently people don't like their software named after serial killers (actually, that was his dog's name). "Lamson Tubes" is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today. Now Lamson is a fully functioning SMTP server, relay, proxy, and e-mail application framework. It supports most RDBMS, has templates, and is easy to manage and deploy. Lamson is smarter than most any e-mail processing system out there thanks to Python. However, as great as Lamson is for processing email intelligently, it isn't the best solution for delivering mail. There is 30+ years of SMTP lore and myth stored in the code of mail servers such as "Postfix":http://www.postfix.org/ and "Exim":http://www.exim.org/ that would take years to replicate and make efficient. Being a practical project, Lamson defers to much more capable SMTP servers for the grunt work of getting the mail to the final recipient. I currently use Lamson in my own work, but I'm always looking for people doing interesting things with e-mail. Even if you're a spammer, or someone trying to destroy the spammers, I want to hear from you. If you've got something interesting, feel free to "contact me":/contact.html and talk about it. lamson-1.0pre11/doc/lamsonproject.org/input/blog/0000755000076500000240000000000011313464573021350 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-16.txt0000644000076500000240000000146011203540277023144 0ustar zedshawstafftitle: Lamson Project Site Launched Today I launched the Lamson Project site at "lamsonproject.org":http://lamsonproject.org/ and started filling in the content. Lamson is really turning into a fun and useful project, and hopefully the site will get other people interested in it and using it. I took the design from one of the many free web design sites and reused the same Python blog script that I use on "my own site":http://zedshaw.com/ so getting this up and running was cake. Subscribe to the "RSS feed":/feed.xml and I'll soon be doing a blog post announcing mailing lists and other email services that I'll host on lamsonproject.org as well as new documentation and code drops. If you browse around you'll find some useful documentation, but nothing complete just yet. I'm still writing most of it. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-18.txt0000644000076500000240000001014411213057130023135 0ustar zedshawstaffTitle: Bug Fix 0.8.4, Mailing Lists, Spam Blocking A few announcements from my work on Lamson the last few days. I managed to fix a bug, put Lamson to work doing Lamson's mailing lists, and use Lamson to do some spam blocking on my own email account. Hopefully eating my own dogfood won't be too painful. h2. Grab 0.8.4, Important Bug Fix I wasn't using the SMTPServer class in Python correctly, and was stopping the whole server when a single channel had an error. It's a one line fix and I've been running it for a few days with no problems. Please make sure that you upgrade if you installed Lamson: sudo easy_install --upgrade lamson Or you can "grab it from PyPI":http://pypi.python.org/pypi?name=lamson&version=0.8.4&:action=display h2. Mailing Lists Lamson is now running its very simple _examples/mailinglist_ sample on lists.lamsonproject.org. You can send an email to: lamson.users-subscribe@lists.lamsonproject.org And try it out. The software running that is also what's available in the _examples/mailinglist_ sample (minus a few tweaks) so if you want to help make it a real functioning mailing list then feel free to contribute. I'm subscribed there, and I'll be working on it over the next week to turn it into a more complete and robust mailing list. An important thing I need to implement is bounce and vacation detection. I'll probably also take my spam filter hacks and work them in as well. Also, the software at lists.lamsonproject.org is an open mailing list system. Feel free to use it at your own risk, and if it becomes popular then I may turn it into a permanent thing. As an open mailing list system, what it does is creates any list that doesn't exist when you subscribe. Think of it as lazy loading for mailing lists. No need to fill out forms, beg for permission, alter aliases, edit text files, or run a ton of shell commands. Just subscribe and go. Now, if that turns into a massive abuse vector then I'll turn it off, but I'd like to make it work for people since it demonstrates something simple that Lamson can do which other mailing list systems are bad at. h2. Spam Filtering And Graylists In The Works My personal email account (zedshaw@zedshaw.com) is now running "Spambayes":http://spambayes.sourceforge.net/ inside of Lamson to filter spam. Here's the code (stripped down for show):
class SpamHandler(server.MessageHandler):
    """Uses app.spam to mark all messages it receives as Spam."""
    def process(self, session, message, args):
        filter = spam.Filter()
        filter.train_spam(message.msg)
        filter.close()

class FilterHandler(server.MessageHandler):
    """
    Uses the spam filter to either queue up a message
    into the run/queue for later review, or forward 
    it on if it is not spam.
    """
    def process(self, session, message, args):
        classifier = spam.Filter( )
        classifier.filter(message.msg)
        if message.msg['X-Spambayes-Classification'].startswith('spam'):
            q = queue.Queue('run/spam')
            q.push(str(message))
        else:
            self.relay.deliver(message)
This shows Lamson's raw "handler style" of extension, which the Finite State Machine stuff is built on. These are wired into the config/settings.py file routing so that any mail that's sent to zedshaw@zedshaw.com goes through *FilterHandler* and any mail that sent to bogus addresses @zedshaw.com goes through *SpamHandler*. It turns out that a lot of spam is delivered to random addresses at zedshaw.com, so this works as an autotraining mechanism. Well, at least until some asshat figures out he can game my spam filter. The FilterHandler then takes messages that SpamBayes knows is spam and puts it into a queue for later review. I periodically check that folder and pull out any false positives for retraining. It's all very hacky right now, but seems to be working really well, so once I work out the full workflow I'll formalize this code and add it to Lamson as extra handlers to use. I'll also implement a simple graylisting system and include that too. Well, enjoy the work so far and shoot me any comments you have. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-19.txt0000644000076500000240000000274111204561532023150 0ustar zedshawstaffTitle: New Site Look, Same Great Content This is just a quick update to say thanks to "Ken Keiter":http://kenkeiter.com/ for creating a new "lamsonproject.org":http://lamsonproject.org/ site layout and design. The new site should be easier to read, have more breathing room, and look easier on the eyes. It's even got a logo: !http://lamsonproject.org/images/lamson.png! Which I like quite a lot. Feel free to shoot your comments about the design to "me.":/contact.html h2. Two Days Of Spam Filtering So far the new Lamson based spam blocking and filtering is working pretty well. The majority of spam is blocked, however the success rate of "SpamBayes":http://spambayes.sourceforge.net/ needs to improve. Right now I've trained it with a large set of ham, and with a reasonable number of spam messages as I encounter them. I think it's received about a 20/1 (ham/spam) ratio for training. With that training it's sorted about 50 spam, no false positives, and missed about 15 spam as "unsure", with about 4 as "ham". The 4 that were classified as ham were just one liners with a link in them. The unsure spam probably could have just been treated as spam, but I'm being conservative while I test it out. Hopefully the success rate improves over the next few days. SpamBayes is great for being a Python library I can use easily, but if it's success rate doesn't get much higher then I may look at other options. Of course, there are more important things I could be doing right now too. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-20.txt0000644000076500000240000000040411205152004023121 0ustar zedshawstaffTitle: Lamson Project Ideas I wrote a "blog post about project ideas for Lamson":http://zedshaw.com/blog/2009-05-20.html on my personal blog. Head on over if you're looking for something to hack on, or just want something to read that isn't about the web. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-24.txt0000644000076500000240000001232211213057130023132 0ustar zedshawstaffTitle: Features For The 0.9 Release (Soon) Content-Type: text/html

I've been hard at work cooking up the very nice new routing system, and I must say it is rather tasty. I've gone and created a whole new routing and state management design that uses decorators right in your handler modules to indicate how each state will expect mail addresses.

For the 0.9 release happening tomorrow I've got a new Router setup, spam filtering baked in nice and clean, improved test coverage, and indirect state storage for those who don't like SQLAlchemy.

New Routing Decorators

The new routing design is very nice if I do say so myself. It should reduce the amount of Python regex wizardry you need to learn, help set reasonable defaults, and put your handlers and routing in one spot so they are easier to maintain. In addition the new design eliminated many files and flaws from the previous design.

Here's a quick sample from the unit tests:

from lamson.routing import Router, route, route_like

Router.defaults(host="test.com", 
                action="[a-zA-Z0-9]+",
                list_name="[a-zA-Z.0-9]+")


@route("(list_name)-(action)@(host)")
def START(message, list_name=None, action=None, host=None):
    print "START", message, list_name, action, host
    if action == 'explode':
        print "EXPLODE!"
        raise RuntimeError("Exploded on purpose.")
    return UNKNOWN
    

@route("(list_name)-(action)@(host)")
def UNKNOWN(message, list_name=None, action=None, host=None):
    print "UNKNOWN", message, list_name, action, host
    return NEXT


@route_like(UNKNOWN)
def NEXT(message, list_name=None, action=None, host=None):
    print "NEXT", message, list_name, action, host
    return CONFIRM

@route_like(UNKNOWN)
def CONFIRM(message, list_name=None, action=None, host=None):
    print "CONFIRM", message, list_name, action, host
    return END

@route("(anything)@(host)", anything=".*")
def END(message, anything=None, host=None):
    print "END", anything, host
    return START

The formatting on the @route decorator will hopefully simplify the use of regular expressions. Rather the above route for "(list_name)-(action)@(host)" gets translated by Lamson into '^(?P[a-zA-Z.0-9]+)-(?P[a-zA-Z0-9]+)@(?Ptest.com)$' which is a hair pulling monstrosity. It also is using defaults so that you only have to indicate basic formatting of the email and leave the regexes to the main configuration file.

This new setup also has some interesting implications in how you can use it. For example, you can register multiple address forms for each state (UNKNOWN, END, etc.) so that it can handle similar activity.

Spam Filtering Per State

Now that the routing is nice and generic, I could implement a decorator for using SpamBayes to filter spam to your state functions. If you don't want a particular state to receive spam then you just use the @spam_filter decorator:

from lamson.routing import route, route_like
from lamson.spam import spam_filter

ham_db = "tests/sddb"

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue")
def START(message, **kw):
    print "Ham message received. Going to END."
    return END

@route_like(START)
def END(message, *kw):
    print "Done."

This simply attaches the @spam_filter decorator to START and then when START runs it transitions to END. It's still a little rough since you're having to configure the @spam_filter right there, rather than in a config/settings.py file, but it works. Later versions will be much cleaner.

Indirect State Storage

I've also abstracted away the state storage, which will open the door to backends in any kind of storage you want, not just SQLAlchemy. The only thing you'll need to implement to use a different storage is a simple get/set/clear set of functionality and then attach it to the Router class to make it use the new storage.

Currently the code is using a simple MemoryStorage class to speed up the unit test runs, and then I'll implement the SQLAlchemy store and probably a Tokyo Tyrant store too.

Higher Test Coverage

I also worked on getting the unit test coverage up as high as I could. Since Lamson is a server with nasty OS level things like sockets and daemons, it is difficult to fully test in just simple unit tests. It's at about 85% right now with only the daemon and server commands not being tested.

Finally, this new design gets rid of the old class based handlers, the Conversation style of Finite State Machine handlers, and quite a bit of code that was design cruft from my previous iterations. It should be much cleaner and meaner, and will have a nicer experience for developers.

Hold On Though

The code in the bzr branch is still in a state of flux, so feel free to grab it and look, but you probably don't want to actually do anything with it. I should have all the samples converted to the new style and then I'll do a 0.9 release. It should happen tomorrow (May 25th).

I'll have another blog post and some documentation for the new stuff tomorrow and all next week.

lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-28.txt0000644000076500000240000000564311210151522023143 0ustar zedshawstaffTitle: 0.9-pre2 Up For Testing, Docs Too First off, my apologies to everyone if your RSS reader went crazy today. I include documentation changes in the RSS feed so that people can easily track updates to the Lamson docs. However, that means when I'm writing a lot of documentation it hits the feed repeatedly. The good news though is that you can now get at the 0.9-pre2 release of Lamson from the "releases":/releases/ page and you can read the great new documentation I've got. Simply go to the "documentation":/docs/ list and go through. You'll want to check out "how to install Lamson in a virtualenv":/docs/lamson_virtual_env.html by Zachary Voase. You'll also want to read through the current draft of the "getting started":/docs/getting_started.html as it covers all the nitty gritty things you need. h2. Migrating To 0.9 I'm getting the 0.9 pre-releases out so that people can start migrating and feeding me problems they encounter. I'll have a full list of changes and some documentation on migrating to 0.9. The main things that changed are covered in the "getting started":/docs/getting_started.html documentation, notably how Lamson is configured and how routing works. Routing is the biggest change of all so read through it. I'll have documentation on exactly how Routing works and how to use it too coming soon. If you started using Lamson at the 0.8.x level and need help migrating "contact me":/contact.html and I'll help you out. Migration should involve just tagging your state functions with the right route decorators. h2. Mailing List Is Dead, Long Live OSB I really didn't like the way I did the mailinglist example, so rather than keep it alive I just killed it and will rewrite it after 0.9. In its place is the _examples/osb_ application which is a One-Shot-Blog. You send it an email and it makes a blog page and index for it. It's simple enough for everyone to comprehend and I'll be able to cover it in documentation easily. The OSB sample has all the right stuff for you to see how all of Lamson's features work, so check it out and use it as the basis for your own applications. You can get it by "downloading the source":/releases/ and looking in the examples/osb directory. h2. 0.9 Probably This Weekend I want to get all the documentation done for the 0.9 release and finish the *very* last feature that it needs. I'm hoping to have that in the next few days, at least by Monday. The only feature remaining for 0.9 is a SQLAlchemy based state storage, and maybe a shelf based one. Lamson now stores its state in an abstract "StateStorage":/docs/api/lamson.routing.StateStorage-class.html class that lets you store state however you need. Right now all that's implemented is a "MemoryStorage":/docs/api/lamson.routing.MemoryStorage-class.html which actually works well enough for most applications that you just start off, and is great for unit testing. But, there needs to be a permanent state store support. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-05-31.txt0000644000076500000240000000203711210533442023134 0ustar zedshawstaffTitle: Lamson 0.9 Later Today I have been working hard on the documentation and scrubbing the code for Lamson and the 0.9 release coming out soon. The only things I feel I need to do before an official 0.9 release are: # Clean up the one-shot-blog demo application a bit more. # Write a small set of instructions on writing your own "StateStorage.":http://lamsonproject.org/docs/api/lamson.routing.StateStorage-class.html # Do a last final code review to check for any obvious problems. Once I do that I'll make 0.9 official. If you're using 0.8.x right now, or just playing with Lamson, please go grab the "0.9 pre-release":/releases/ and make sure it works. There is one bug in that 0.9-pre3 release where the router won't properly route of the form:
To: "First Last" 
This is fairly rare, since someone would have to craft an email like this, and it would just be ignored anyway. *That bug is fixed in the current 0.9 source.* Please shoot me any bugs you find and I'll work them into the 0.9 release. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-01.txt0000644000076500000240000000737511210662772023155 0ustar zedshawstaffTitle: Lamson 0.9 Is Out, Find My Bugs! I just pushed Lamson 0.9 up to PyPI for everyone to grab and break. This release features a complete redesign of the routing, state handling, templating, and a full set of very complete documentation. Everyone who was using 0.8.x series should be able to migrate to this version with some work, but it won't be terribly painful (assuming you have unit tests). h2. Getting 0.9 Easiest way to install is with "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall straight from PyPI:
sudo easy_install lamson
There's also instructions on "installing to a virtualenv":/docs/lamson_virtual_env.html for those who like that. Best thing to do is read the "Getting Started":/docs/getting_started.html instructions and when you're done with at least the 30 second introduction you'll have your Lamson. Try to go through the whole document if you can and send me feedback. h2. Big Changes The biggest change is that the routing mechanism and internals are totally different. Now instead of putting all your routes into the config/settings.py as a big array, you just put them on each state function as a decorator. To help you debug these fancy routes there's also a new @lamson routes@ command that will dump them in their final state. The biggest impact of the new design is the old school "BlahHandler" classes are gone. Later versions of Lamson might bring back the ability to use a class as a handler. After that comes the removal of depending on SQLAlchemy for state storage. You can now "write your own state storage":/docs/writing_a_state_storage.html and store things however you need to whatever you need. There is currently a simple storage for storing in memory and for using a Python shelve dict. Next there was a change to how Lamson boots up and configures itself giving you more control over what it uses, and giving your handlers better access to your gear. This change also let's you use better unit tests that don't require a running lamson server, as well as giving you the old "integration test" style tests that hit your real server. There's also tentative support for PyEnchant spell checking of your templates, and a new @lamson spell@ command to help you use it. See the "getting started":/docs/getting_started.html document for how to configure that, but be warned that PyEnchant is kind of a mean install. Finally, I switched to Jinja2 templates by default, but you can use Mako fairly easily by altering a configuration variable in the boot.py. h2. Look At All The Shiny Docs In addition to redesigning the guts of Lamson so that it's easier to work on and use, I also spent a lot of time cranking out good documentation so people can get started using it. Take a look in the "documentation":/docs section and have a gander at the "API documentation":/docs/api/ that is generated from the source documentation. h2. New Example: One-Shot-Blog In the source distribution "you can grab here":/releases/ there's an example of doing a simple blog that uses just email to manage the blogger's posts. The example replaces the mailing list example (for now) as it is simpler and doesn't use a database. h2. Please Break It The 0.9 release is the result of a lot of work, but I want people to break it and criticize it to death. The goal is to have people beat up this basic functionality, producing a series of 0.9.x releases, and then when everything is tight doing a 1.0 release. h2. Thanks Everyone I'd like to thank everyone on Twitter, Hacker News, and yes, even Reddit for their comments both good and bad. It helped me make good decisions knowing what people wanted, so keep the feedback coming. Thanks also to those who donated documentation, praise, or other help directly to the project. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-03-2.txt0000644000076500000240000000504411211630350023271 0ustar zedshawstaffTitle: Lamson 0.9.2, Test Coverage 97% The 0.9.2 release is out and ready for everyone to easy_install. I spent the day getting rid of my tech debt by boosting the Lamson test coverage to a whopping 97%. I also wrote new documentation on "Running A Queue Receiver":http://lamsonproject.org/docs/deferred_processing_to_queues.html in a separate process using Lamson. h2. Test Coverage 97% I used the very awesome "mock":http://www.voidspace.org.uk/python/mock/ library which let me mock out a bunch of the system calls that the "lamson.commands":http://lamsonproject.org/docs/api/lamson.commands-module.html module accesses to do its work.
Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               120    120   100%   
lamson.commands           185    178    96%   199-200, 347-349, 367, 369
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.mail               122    121    99%   234
lamson.queue               30     30   100%   
lamson.routing            185    184    99%   417
lamson.server              85     84    98%   182
lamson.spam                80     74    92%   38-40, 59, 65-66
lamson.utils               53     50    94%   57, 70-71
lamson.view                15     15   100%   
-----------------------------------------------------
TOTAL                     885    866    97%   
----------------------------------------------------------------------
Ran 84 tests in 7.056s
By mocking out things like @sys.exit@ and sockets I'm able to run the code and make sure those methods were called correctly. I could also throw exceptions to make sure that exception handlers ran correctly. h2. Two Bugs Fixed Interestingly enough, increasing the test coverage to 97% didn't find any more bugs. I did find one small bug and one obvious bug in the 0.9.1 release. # If you gave a trailing option that wasn't a string it would throw an exception. # If you tried to start with a different boot script then it wouldn't do it. These are now fixed in the 0.9.2 release, so make sure you "grab it":/download.html with the easiest method being:
$ sudo easy_install lamson
h2. IRC Channel on irc.freenode.org I'm now hanging out in the #lamson channel on irc.freenode.org. Anyone who wants to come and chat about lamson can stop by. I should be there all the time, but I may not respond immediately. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-03.txt0000644000076500000240000000362311211415751023141 0ustar zedshawstaffTitle: Lamson 0.9.1 Out, New Docs I released Lamson 0.9.1 today so please "grab it and test it":/download.html and shoot me feedback. The little changes are easier handling of attachments in MailRequest, a small tweak to spam_filter, and then a bunch of test enhancements and extra gear. The big change is a redesign to lamson.args so that it infers the command line arguments from the lamson.commands function keyword arguments. It's mostly an internal change since others can't write their own commands (yet). h2. New Documentation I ran into a bunch of folks who didn't know that Lamson had built-in spam filtering, so I wrote a document on "Filtering Spam With Lamson":http://lamsonproject.org/docs/filtering_spam.html that tells you how to use it. Lamson's spam filtering support is fairly simplistic, but still useful for filtering crap out of your handlers. It's biggest problem is the current implementation requires you to use the (barely documented) "SpamBayes" command line tools for things like training. I've got some instructions to get you started with the most important SpamBayes command line tool you'll need for training. h2. ChangeLog Here's the log of what I did: * Big code review, cleanup, and test boosting. * Modified the MailRequest.body so that it works with attachments. * Updated the OSB in preparation for a redesign to make it more interesting. * Didn't like the name 'attachments' for getting what's really all the parts of a message. * Implemented a method on MailRequest to return all of the attachments in a message reliably. * Setup the muttrc to use env to find the lamson command. * Updated the commands and utils so that it works with the new lamson.args setup. * Refactored lamson.args to use inspect to figure out the required defaults of a command function, thus removing the need for a... * Contributed change to muttrc so it uses lamson wherever it is installed, even virtualenv. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-04.txt0000644000076500000240000000462611211760244023146 0ustar zedshawstaffTitle: OneShotBlog Sample (Hack) Running I *finally* got off my ass and put the "OneShotBlog":http://oneshotblog.com/ sample up. This is the code (plus a few little tweaks) from the sample that is in the "Lamson source":/releases/ running on another server. It's using all the features of Lamson, include the new "Queue Receiver":/docs/deferred_processing_to_queues.html functionality. So far it's working great considering I've just been hacking on it on the side to try out the usability of the Lamson "APIs":/docs/api/ and not really taken it seriously. The concept of the oneshotblog is a little weird: # You send an email to SOMETAG@oneshotblog.com, it can have textile markup. ## SOMETAG can be anything that's a tag (chars, ints, periods, no leading periods), like: i.want.my.blog@oneshotblog.com # That is posted for you at http://oneshotblog.com/posts/YOURADDRESS/SOMETAG.html ## I *do* block attempts to use a bad path by whitelisting. If you send something messed up for the tag it's ignored. # Then your post is summarized and tossed on the "front page":http://oneshotblog.com/ with the most recent 50 shown. ## This is done by a deferred queue receiver that indexes messages as it receives them. # If you send to that same tag, it replaces it with the new contents (although the index will show the old stuff for a while). There's some dumb bugs in it currently, like delete doesn't remove your message from the index and other fun things, but so far it's running as expected given the work I put into it. Anyway, feel free to try it out and shoot me feedback. Try sending really broken email too so that I can see what Lamson does with it. h2. Future Features Of OSB Just for fun I'm gonna clean up OSB and make it official. I'll add features like commenting on a post via email, each post is a little mailing list, and handling image and code attachments. For the code attachments I'll colorize them with Pygments and put them up with the post. h2. Mailing Lists and Failmail I'll be bringing back the mailing list example soon, and I'll also make an address failmail@lamsonproject.org. More on that later. h2. Python 2.6 Support If you're running Python 2.6 then you'll want to wait for the 0.9.3 release later today. The deployed OSB sample at "oneshotblog.com":http://oneshotblog.com/ is running on Python 2.6, so I had to fix about 4 failing tests regarding unicode issues and weirdness with python-daemonize. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-06.txt0000644000076500000240000002004311213057130023132 0ustar zedshawstaffTitle: Lamson 0.9.3 Is Out And Sexy As Hell This release is the result of me working on my little "oneshotblog.com":http://oneshotblog.com/ project while tweaking and refining Lamson as I go. The end result is 0.9.3 didn't have a lot of big code changes, but all the tiny little changes add up to a very nice release. The highlights of this release are more secure server runs, better character encoding handling for headers, various cleanups in how mail is queued, and fixes for Python 2.6 support. My plan is to keep using this release, improving what is there currently and only adding minimal features, and just making what is there tighter. The goal is to *have a 1.0 release out* once I've used it on "oneshotblog.com":http://oneshotblog.com/ and the new "lamsonproject.org mailing lists":/lists/ application. h2. Getting It (As Usual) All you need to do to get lamson is read the "download instruction":/download.html and then go through the "getting started":/docs/getting_started.html to try it out. The *docs may be out of date with this release*, but I'm going through docs tomorrow. h2. NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM I'll be presenting Lamson at the "NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html and helping people get started with it. If you wanted to try it but didn't want to waste the time then come down and I'll hook you up. h2. OneShotBlog Sample Is Live Lamson includes the source to the "oneshotblog.com":http://oneshotblog.com/ project in the @examples/osb@ directory. You can "grab the source tar.gz":/releases/ and look in there fore the source. You should also try out the sample site to help test out Lamson, especially if you use a weird character set (since that's the biggest weakness currently). This release of OSB features a bunch of bug fixes, tons of tests that improve the code coverage, the ability to comment on a post by sending an email, decent spam filtering of the comments, ability to flag a comment as spam, and lots of little tweaks. It uses *all* of the features Lamson has right now. It still has some work to do, and I want to add the ability to send pictures and source code (with syntax highlight). h2. The ChangeLog The change log for this release is (with the big ones bold): * Cleaned up some stray printing and logging. * Now prints a nicer message if args get a bad argument. * *Managed to get a file object out of the maildir which removes the need to reconvert from a message.* * A few minor test suite tweaks to get coverage on the most recent features. * *Implemented a reorg of how the servers are started so that they can bind port 25 and then drop priv to a safer user.* * *Initial support for an undeliverable queue and a forwarding handler that you can run in a queue receiver to deliver unhandled mail to the relay. Needs tests.* * Using the new Router.UNDELIVERABLE_QUEUE feature to have bad mail go to a queue. * Fixed the queue_command so that it prints what queue it is working with. * Added a setting for the Router to tell it to dump undeliverable mail to a queue. * *First cut at semi-automated header encoding conversion.* * Initial support for sort-of-automated header decoding/encoding on MailRequest. * Found a bug in clear_queue which made it not able to clear alternate queues. * Small changes to fix a non-idiomatic has_key usage. * Tweaked testing so that it resets the Router state when you do a client.begin() in unit tests. * Tweaked the boot vs. test logging setup to make it nicer for both situations. * *Since the Router is the primary code loading mechanism, it now uses its own separate logger that you can redirect to stderr to see loading errors during dev.* * *Created a disconnected set of config testing gear so that it can test without relying on any examples. Also configures the logger using a config file and cleans up test output.* * *Added a feature to the routes command to print out how each matching regex will match a given test address.* * Added an exception handler for loading and reloading handlers so that you can see which ones are broke during development. * Added a -test feature to lamson routes that let's you test an address to see what routes it matches. * *Updated for Python 2.6 so the tests will run, not sure why they pass in Python 2.5 actually.* * Modification to the mail parsing so that it forces 'ascii' encoding to keep email.message_from happy in Python 2.6 h2. Using The Drop Privilege Lamson can now drop privilege after binding port 25. Previously it was too difficult to make the python-daemonize library do it correctly, but after I reorganized some code I was able to do the privilege drop manually to implement it. What this means is you can run your email server and *stop being root* right after you bind port 25. To do this, you'll just run your server with:
$ sudo lamson start -uid 500 -gid 500
$ cd ..
$ sudo chown -R someuser.someuser yourapp
You technically don't have to fix the permissions, but if you use "queue receivers":/docs/deferred_processing_to_queues.html then you'll need to change the permissions back. Lamson still creates a few files as root before dropping to your given UID and GID, but hopefully it will stop doing this soon. Obviously, you might want to know what your UID and GID is, but it's important that you know what *Python* thinks it is:
Python 2.5.1 (r251:54863, Jan 13 2009, 10:26:13) 
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getgid()
500
>>> os.getuid()
500
>>> 
There's other ways to find it out, but that's the Python way. h2. Using lamson routes The @lamson routes@ command has improved a bit so that you can inspect your routing and test out an email address to see how it would be sorted. Here's a sample run:
osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 
If you're working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it'll take an email address and tell you all the routes that match it. The output needs to be cleaned up, but for debugging this is nice. h2. The lamson web Command While working on "oneshotblog.com":http://oneshotblog.com/ I kept having to open up the site that it generates, so I just threw a dirt simple HTTP server into Lamson. Here's a sample of running it:
~ $ cd projects/lamson/examples/osb/
osb $ lamson web -basedir app/data/
Starting server on 127.0.0.1:8888 out of directory 'app/data/'
localhost - - [06/Jun/2009 12:31:11] "GET / HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/reset.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/main.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:18] code 404, message File not found
It just stays there running and printing to the console, so I just use "GNU screen":http://www.gnu.org/software/screen/screen.html and leave it running in one of the windows. h2. As Usual Test And Report Test this release out, and shoot me feedback. I'm hanging on irc.freenode.org again in the #lamson room, so just stop in if you need help. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-08.txt0000644000076500000240000000335511213316150023143 0ustar zedshawstaffTitle: A Screencast And Docs On Deploying Lamson And OneShotBlog I just finished writing some new documentation on "Deploying Lamson and OneShotBlog":/docs/deploying_lamson.html that shows you how to install Lamson and all required software into a completely clean Python 2.6 and virtualenv configuration. The instructions take you from nothing to a running "oneshotblog.com":http://oneshotblog.com/ installation that you can play with and hack on. The process of doing Lamson deployments is still a little too rough for me, but these instructions should get people started. I also created the first screencast "teaching you how to do the same deployment":/videos/ but stopping at installing "oneshotblog.com":http://oneshotblog.com to keep things short. It is basically me talking and typing into a terminal window to 12 minutes showing you how to do it. You can also "download the video":http://www.archive.org/details/Lamsonproject.orgDeployLamsonInAVirtualenv from "archive.org":http://archive.org/ to get a better viewing experience. Being my first screencast I can say that it is very tedious. I'll be working on various strategies to make the process more streamlined and easier since having to take a whole day for each one will drive me insane. If you have suggestions on making the video better, "let me know":/contact.html and also if you know of tools to help clean up video. One thing I know right away needs to improve is the sound quality. An apartment in NYC is just too damn noisy for this kind of thing. Enjoy, and don't forget that I'll be "presenting Lamson at the NYLUG Python workshop tomorrow June 9th at 6:00pm":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html so come on down if you want to get your Lamson working. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-09.txt0000644000076500000240000000102011213542615023136 0ustar zedshawstaffTitle: Lamson At NYLUG Python Workshop Today @ 6:00PM Just a quick reminder that I'll be presenting "Lamson":http://lamsonproject.org/ to the NYLUG Python Workshop today at 6:00PM. The event is at the NY Public Library Hudson Park Branch, 66 Leroy St., NY NY 10014 in NYC and you can "find out more here.":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html I'll be showing people how to set up Lamson, get it installed, do cool stuff with it, and then just answering questions and helping people work with it. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-14.txt0000644000076500000240000002157011215276254023153 0ustar zedshawstaffTitle: The Mailocalypse Is Upon Us! I'm currently polishing off the two final features before I start going for the Lamson 1.0 release. I've been using Lamson to make a few little cute applications and create one thing for a potential client, and so far I haven't had to change much since 0.9.3. It's great so far and I hope that Lamson 1.0 will be a fun release. The *big* thing that must improve though is handling character encodings in emails. After spending a week or more trying to come up with an automated conversion scheme that would honor all the encodings on the planet, I had a realization. h2. Why Isn't All Mail UTF-8? Remembering the purpose of Lamson as a *modern* mail server and framework, it finally struck me as dumb that I'm trying to keep around encodings that were invented in the pre-Unicode days. Every modern system understands and displays UTF-8, and apart from some pissy attitudes from the Japanese about the Han unification, there's really no reason that every email Lamson handles can't be "upgraded" to UTF-8. As I thought about this more I started to like the idea. Take all mail Lamson handles, and upgrade it to utf-8. Then inside Lamson you know that it's future proofed and ready to be used by Python no matter what. Finally when the mail goes out, the conversion for sending is simply to encode the email as proper UTF-8 encoding consistently. However, I'm not the type of person to think any random idea I have is instantly valid because I thought it up. Before I make a grand announcement that Lamson only deals with UTF-8 I better know that it's going to have minimal impact on people using it. I made a similar decision with Mongrel, where I chose *not* to accept clients that didn't follow the standard, and it turned out to block a ton of security attacks for free. What if Lamson's required conversion to UTF-8 could not only simplify Lamson mail processing, but also prevent spam and security flaws? I decided to do some basic analysis and find out. h2. This Is The Mailocalypse I've decided that Lamson will *NOT* try to maintain the encodings it is given by a client. It will consider all encodings suspect, and attempt to convert them to UTF-8 as a means of cleaning and simplifying the mail it receives before it talks to other systems. Every other system you'd most likely talk to is either UTF-8 capable or even requires it. I'm calling this "The Mailocalypse" because it's funny, but it will also separate Lamson from other systems, in potentially awesome or horrible ways. To validate this premise, I created a small Python script that takes a Maildir or mbox and tries to convert every email into a UTF-8 encoding. It's not trying to write the emails, just load them and convert every single piece reliably to UTF-8. It then reports any emails that have a problem with the conversion. After that I ran it on a bunch of mail I had and got other people to run it and report their findings. bq. "You can grab the mailocalypse.py script here.":/mailocalypse.py Improvements welcome. The data I got from a small sample of the mail (randomly selected) was the following:
all bad spam
5069 7 0
263 15 1
1806 231 1
2650 69 0
4509 20 0
2023 495 1
3723 74 0
bq. all is the total number of messages in the mailbox (including bad). bad is the number that were bad. spam is whether that box was a spam box or not (meaning all messages were classified as spam). I also made sure that the characters showed up in web browsers and in various UTF-8 mail clients and terminal windows. Most of the conversion was reliable and very fast for a quick script. With the above data, there's two things you can notice right away, and which are confirmed with a quick R session: # Spam fails to convert more frequently than ham messages. It looks like failure to convert is a 0.03 significant indicator that the message is spam. I'd need more data to confirm that. # Legit mail (ham) only has a 1.2% chance of failing to convert, and a quick eye-ball sample says those failures mail are mostly spam that wasn't classified right. h2. Potential Problems This analysis shows initially that converting mail to UTF-8 could work, but there are two problems I see so far: # I may not be doing the best conversion in that script. # Encryption and signatures will screw this up. For the first problem I'm posting the script and asking people to check it out and let me know if there's problems. Run it on your stuff and shoot me the ALL/BAD numbers when it's done. Check the code and see if you can improve it. Whatever you can to show that converting to UTF-8 is a good *OR* bad idea. A key design decision though will be that you never lose the original email, only that it is converted for Lamson so you can work with it in your code without it blowing up. If the mail can't convert, then it's bad email and should be rejected anyway. No point trying to process it. For encrypted email or ones with signatures you would just run all the validation on the original, or forward the original on, depending on your application. For example, a mailing list would still want to convert all mail to UTF-8 for processing, posting to web sites, spam filtering, and rejecting potentially bad email. Signed and encrypted email would then just be forwarded on in original form, or decoded and validated depending on the how the mailing list works. h2. Advice And Criticism Welcome Take the mailocalypse script (presented below for review) and try it out. Confirm that it works on most of your mail, and send me the results. Simplest way is to send me a message on twitter like: bq. @zedshaw ALL 2340 BAD 17 spam yes #mailocalypse And I'll tally the results and do a better analysis. You are also encouraged to fill me in on what I'm missing in the idea. Realize that *part* of the idea is to eliminate uncommon corner cases and to prove that a situation is common using evidence. Try to follow the same model by running the script on your mail to prove that it won't convert reliably or devise your own script to demonstrate your thoughts. h2. The Codes "You can grab the mailocalypse.py script here.":/mailocalypse.py Improvements welcome.

import email
from email.header import make_header, decode_header
from string import capwords
import sys
import mailbox


ALL_MAIL = 0
BAD_MAIL = 0


def all_parts(msg):
    parts = [m for m in msg.walk() if m != msg]
    
    if not parts:
        parts = [msg]

    return parts

def collapse_header(header):
    if header.strip().startswith("=?"):
        decoded = decode_header(header)
        converted = (unicode(
            x[0], encoding=x[1] or 'ascii', errors='replace')
            for x in decoded)
        value = u"".join(converted)
    else:
        value = unicode(header, errors='replace')

    return value.encode("utf-8")


def convert_header_insanity(header):
    if header is None: 
        return header
    elif type(header) == list:
        return [collapse_header(h) for h in header]
    else:
        return collapse_header(header)


def encode_header(name, val, charset='utf-8'):
    msg[name] = make_header([(val, charset)]).encode()


def bless_headers(msg):
    # go through every header and convert it to utf-8
    headers = {}

    for h in msg.keys():
        headers[capwords(h, '-')] = convert_header_insanity(msg[h])

    return headers

def dump_headers(headers):
    for h in headers:
        print h, headers[h]

def mail_load_cleanse(msg_file):
    global ALL_MAIL
    global BAD_MAIL

    msg = email.message_from_file(msg_file)
    headers = bless_headers(msg)

    # go through every body and convert it to utf-8
    parts = all_parts(msg)
    bodies = []
    for part in parts:
        guts = part.get_payload(decode=True)
        if part.get_content_maintype() == "text":
            charset = part.get_charsets()[0]
            try:
                if charset:
                    uguts = unicode(guts, part.get_charsets()[0])
                    guts = uguts.encode("utf-8")
                else:
                    guts = guts.encode("utf-8")
            except UnicodeDecodeError, exc:
                print >> sys.stderr, "CONFLICTED CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except LookupError, exc:
                print >> sys.stderr, "UNKNOWN CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except Exception, exc:
                print >> sys.stderr, "WEIRDO ERROR", exc, part.get_charsets()
                BAD_MAIL += 1


            ALL_MAIL += 1

mb = None

try:
    mb = mailbox.Maildir(sys.argv[1])
    len(mb)  # need this to make the maildir try to read the directory and fail
except OSError:
    print "NOT A MAILDIR, TRYING MBOX"
    mb = mailbox.mbox(sys.argv[1])

if not mb:
    print "NOT A MAILDIR OR MBOX, SORRY"

for key in mb.keys():
    mail_load_cleanse(mb.get_file(key))

print >> sys.stderr, "ALL", ALL_MAIL
print >> sys.stderr, "BAD", BAD_MAIL

lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-20.txt0000644000076500000240000001045011217126716023142 0ustar zedshawstaffTitle: Lamson 0.9.4 With Unicode Super Powers Lamson 0.9.4 is out and it's sporting a completely rewritten and meticulously crafted encoding system. With the new "lamson.encoding":http://lamsonproject.org/docs/api/lamson.encoding-module.html code Lamson can now decode nearly any nasty horrible encoded spam or mail you hand it, turn it into pristine nice Python unicode strings, and then output sweet clean ascii or utf-8 in a consistent way. The purpose of this new encoding system is to make sure that Lamson is giving your handlers the best input it can, based on the assumption that the world is evil and Lamson will be handed utter garbage. In order to pull this off, Lamson actually attempts to decode everything it gets, and uses "chardet":http://chardet.feedparser.org/ to guess whenever it can't. It goes far enough to make sure that 99.9% of legit mail gets through, and the rest is usually spam. However, Lamson is so good at this conversion now that it even does it properly on about 99% of spam, even more. It doesn't matter what character encoding is used, if there's a mix of encodings or even if something lies about the encoding or doesn't give one. Lamson can figure this out and convert it anyway, and the rest is junk. You can look at this "image of a spam inbox":/lamson_vs_spam.png and "this image of a spam inbox":/lamson_vs_spam_2.png where you can see a screen session showing this off. This is a split screen with a Mutt on top showing badly formatted spam, and then a Mutt on the bottom with the results of Lamson's cleansing. This is just running in iTerm, but looks the same in most unicode enabled terminals and clients. The interesting side effect of Lamson's decoding is that it undoes almost all of the spam obfuscation techniques quickly and consistently. h2. There Will Be Bugs I've thrashed the hell out of this code and made sure that I really took the time to get it right. I've ran it on thousands and thousands of real and spam mail and tested the crap out of it with unit tests and fuzzing. Still, this is new code handling email so there will be bugs in it. If you run into an email that you think should parse correctly, send it to me and I'll look at it. h2. How It Works All Lamson does is completely parse every header and figure out how to decode it, but it assumes that the client is a liar. If the string decodes without errors then Lamson is happy, but if it blows up then Lamson uses chardet to figure out what the contents really are encoded as. If chardet can't figure it out, or if it's still busted, then Lamson rejects that mail (which happens less than a fraction of a percent of the time to real email). Once this decoding process is finished Lamson has converted the email completely into Python unicode only. You can work with it in a modern way without worrying about the original codecs, and you can set your headers and attachments without worrying about setting the codecs because Lamson will use the same tech to get the encoding right. When Lamson writes an email, it assumes that your text can be encoded as either plain ASCII (as most headers are), or UTF-8. If it can't, then your text probably can't be processed by Lamson anyway. Lamson will favor ASCII first since it's easier, and then use UTF-8 for anything that can't be ASCII. bq. During this conversion process anything that's *not* text is treated as raw binary and just decoded as-is. By doing this Lamson produces nice clean email that can be easily processed and passed around, and which you can review and debug easier. It also turns Lamson into a "modernizing" agent since it is producing the email it wants to see. h2. Getting This Release I had some issues pushing this release to PyPI, so let me know if it barfs on you. Otherwise, you can hit the "download":/download.html section and grab the gear. h2. The New "Cleanse" Command In order to make it easier for people to try this new cleansing system on real email I've written a quick @lamson cleanse@ command. What this command will do is take a mbox or Maildir, and run it through the washing machine, writing the results to a different maildir. When it's done you can go look at the reasons why some mail failed, how many failures, and you can actually open that maildir mail and look at it with your client. Try it out and report any errors you find. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-22.txt0000644000076500000240000000776511220041004023137 0ustar zedshawstaffTitle: 0.9.5 Almost There, But Stumped On Templates Since the 0.9.4 release I've rewritten the main part of the decoding parser so that it's much cleaner and handles more edge conditions. If there's one word that defines what makes MIME horrible it would be "edge". It's amazing the kind of stupid crap clients are allowed to send out in headers. I've actually got test cases from Mutt where it breaks headers up into completely different encodings across multiple lines for no damn reason at all. Here's a test I had to write just to cover the one worst case I found:
def test_dumb_shit():
    # this is a sample of possibly the worst case Mutt can produce
    idiot = '=?iso-8859-1?B?SOlhdnkgTel05WwgVW7uY/hk?=\n\t=?iso-8859-1?Q?=E9?='
    should_be = u'H\xe9avy M\xe9t\xe5l Un\xeec\xf8d\xe9'
    assert_equal(encoding.header_from_mime_encoding(idiot), should_be)
If you can decode what that actually is and why it's retarded then you'll win a prize. I'll give you a hint: @B != Q@. The example of the above working is "on this page at oneshotblog.com":http://oneshotblog.com/posts/zedshaw@zedshaw.com/test.html so that's solved (for now). h2. Now About Those Templates I originally wanted to make writing templates as easy as writing an email or a web page, so I let you do things like this:
To: {{ message['from'] }}
From: {{ confirm_address }}@{{host}}
Subject:  Please confirm your new blog {{ post_name }}
Reply-To: {{ confirm_address }}@{{host}}

Hi there, you requested that I create a blog with a post named:

{{ post_name }} : {{ message['subject'] }}
...
This is a "Jinja2":http://jinja.pocoo.org/2/ that "oneshotblog.com":oneshotblog.com sends out when it makes you confirm creating a blog. This seems all normal, since it's the headers you want and the body, like an email, but it is *a total fantasy*. The reason is you can't actually decode the above template if any part of it has non-ascii chars. I'll explain, and then hopefully I can get some suggestions on what people would like. First, Lamson does the right thing and in your templates you are getting perfectly usable and future proof @unicode@ objects to work with in your handlers. Also, Jinja2 has no problem with taking this unicode and processing the above template. It works fine, and Jinja2 or Mako aren't to blame for the problem with templates now. The problem is actually in how email has to be parsed, since the email library expects you to hand it a @ascii@ encoded string, no unicode allowed. Nothing else. Lamson's new code can handle the unicode fine, the problem is actually in this one function:
def respond(template, variables, html=False):
    results = render(template, variables)

    data = email.message_from_string(results.encode('ascii', 'replace'))

    if html:
        msg = mail.MailResponse(Html=data.get_payload())
    else:
        msg = mail.MailResponse(Body=data.get_payload())

    msg.update(data)

    return msg
The above function has to go, but where I'm stuck is that it'd be way more elegant to ditch this whole mess completely and remove headers from the template. It's too hard to write these headers by hand unless they are simple ones, and there needs to be a way to pass in more complex headers that you want to add. If I'm providing a way to pass in headers to @lamson.view.respond@ then why not take the headers out of the template and simplify the whole rendering process? I'd like to hear from people using Lamson or those who might have ideas on this. If taking your headers out of the template and moving them to the render call will totally screw up your day, then let me know. If it comes down to it, and I have to do it, I'll probably write a conversion tool to help. Also, if you have a suggestion for doing the above better but keeping the headers then let me know. Thanks for your help folks, and I'll be hanging out in the #lamson channel on irc.freenode.org if people want to hash out a design. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-06-26.txt0000644000076500000240000000724111221170627023147 0ustar zedshawstaffTitle: Lamson 0.9.5, The Push To 1.0 I just released Lamson 0.9.5 with all the major Unicode refactoring done and working. This is an important release because 0.9.5 is where I declare that I'm pushing to a 1.0 release and the base Lamson "APIs":/docs/api/ won't change under penalty of death. In fact they can't change because I'm using them myself in a few applications. From now on, if an API has to change, then I'll keep the old one around and attach a deprecation warning, and then provide a new one. You'll then have by 1.0 to stop using it and start using the new one. Except for a situations where a bug must be fixed, I can't see the actual Lamson API needing such a drastic change by 1.0. What will happen between now and 1.0 is bug fixing, and adding any nice additional features people need within the current "API":/docs/api/ to help build applications. This would be things like more connectors to other mail protocols (LMTP, QPMP), commands for deployment features, and possibly a way to write your own command. So, if you've been waiting for the API to stabilize then 0.9.5 is the release to grab and start using. I'm currently using it on about 6 little projects and it's working great so far, but I need more people to try it out and send me feedback on how well it works for them. h2. Getting This Release As usual, you can read the "download":/download.html documentation to learn how to grab it. After you grab it, go read these documents: * "Getting Started With Lamson":http://lamsonproject.org/docs/getting_started.html * "A Painless Introduction To Finite State Machines":http://lamsonproject.org/docs/introduction_to_finite_state_machines.html * "Deploying Lamson And OneShotBlog":http://lamsonproject.org/docs/deploying_lamson.html Then you should hit the "API documentation":http://lamsonproject.org/docs/api/ and start writing your application. Keep your first one small and try deploying it in different ways. h2. Simple Virtualhost Deployment Instructions Coming Soon The "oneshotblog.com":http://oneshotblog.com/ sample application is now deployed in a new virtualhost configuration on a new VPS I purchased this week. This configuration is different from the ones given in the documentation above because it allows you to run mulitple applications for multiple domains on a single machine. I'll be writing up how to build your own version of this configuration today, but the quick description is this: # I have postfix running on port 25 now *in front* of Lamson. # Postfix is configured to take all mail for each domain and dump it into a maildir queue for each one. # The Lamson applications are then reconfigured in their @config/boot.py@ to use a "QueueReceiver":http://lamsonproject.org/docs/api/lamson.server.QueueReceiver-class.html to feed itself messages from its maildir. # Lamson then just uses the same postfix server as its relay host like before. The advantage of this configuration, apart from being able to easily host multiple domains, is that you can shutdown your Lamson application and when you start it back up it will catch up on the mail in the maildir. People will probably never know you did maintenance or stopped the server temporarily. The disadvantage of this configuration is that it's a royal pain to make postfix do this. The instructions will show you step-by-step how to pull it off, so hopefully it'll make it easier on people who need it. However, I'm gonna tell people to start small and just use the simpler configuration "Lamson in front" configuration and leave this for the big times. h2. Mailing List Will Go Down Finally, I'll be moving the mailing list shortly, so if you send a message and don't receive a reply then that's why. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-03.txt0000644000076500000240000000233011223705276023143 0ustar zedshawstaffTitle: Article In The Reg About Lamson (By Ted Dziuba) Just a quick update to point people at an article in "The Register":http://www.theregister.co.uk/ by "Ted Dziuba":http://teddziuba.com/ entitled "Lamson - email app coding without the palm sweat: Doing what Java never did":http://www.theregister.co.uk/2009/07/03/lamson/. He interviewed me about Lamson, things I think you can use it for, and other fun stuffs. A snippet from the article: "As a development platform, e-mail has gone neglected for decades. Its esoteric implementation details and specifications are regarded by many in the IT business as voodoo, best left to old-granddad programs like Sendmail or Postfix. Zed Shaw hopes to change that with his new project, Lamson. (The name Lamson is a throwback to the early 20th century pneumatic tubes used to shuttle messages between offices. It was originally called Son of Sam, but Zed's dog convinced him to change it)." "Read the article.":http://www.theregister.co.uk/2009/07/03/lamson/ h2. 0.9.6 Coming Soon I've been cranking on various projects with Lamson and haven't ran into too many other bugs. I'll be doing a code review of what's in the Bazaar repo and then pusing out a 0.9.6 probably this weekend. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-07.txt0000644000076500000240000000032411224721637023150 0ustar zedshawstaffTitle: Lamson Mailing Lists Down I'm making up the release of Lamson and doing some server maintenance so I took down the Lamson server running the mailing lists. I'll let everyone know when they're back up. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-09.txt0000644000076500000240000002202411225570307023150 0ustar zedshawstaffTitle: Lamson's Bounce Detection Algorithm I just finished committing 0.9.6 code for doing bounce detection and analysis with Lamson. It's part of the new mailing list example I'm coding up for the 0.9.6 release which I'll be running on a free mailing list site I'm going to release soon. In this blog post I'd like to go through the bounce detection algorithm and get some feedback and samples from people. So far it works great for the samples I have, but I want it to be fairly bullet proof. What I have here is a quick description of how Lamson is doing bounce detection, and then how it gives you parsed information to do analysis beyond that. I'm hoping that people who have to deal with bounces from systems like MS Exchange can possibly either provide samples or advice so I can tune the algorithm better. h2. The State of RFC 3464 And 1893 The two big RFCs for bounce handling are "RFC3464 - An Extensible Message Format for Delivery Status Notifications":http://www.faqs.org/rfcs/rfc3464.html and "RFC1893 - Enhanced Mail System Status Codes":http://www.faqs.org/rfcs/rfc1893.html which cover the headers and error messages you'll encounter. In RFC3464 there's various headers that are given which a bounce message should have, as well as the double and sometimes triple multipart-mime wrapping you need to do to give back the bounced message. A major pain with this standard is that the headers are spread out across the different nested parts, some might be duplicated, and the format is *never* respected by the various MTAs out there. With RFC1893 you have a large number of status codes, but they are combined in weird ways such that you have a primary code with the first digit, a secondary code with the second, and then a "third" code that's the second and third digit combined. Unlike HTTP error codes where there's just a single number mapped to a single status code, in RFC1893 it's this strangely nested first+second+(second+third) thing. Adding to this is there's a few headers where you can put these status codes, and in various formats. Here's a sample of the *parsed* Diagnostic-Code from a gmail bounce message:
 'Diagnostic-Code': [(u'smtp',
                      u'550-5.1.1',
                      u"The email account that you tried to reach does...")],
The end result is that if you want to determine if a message has bounce (not even hard vs. soft) then you have to go trolling through nested attachments looking for headers and parsing their values on the chance they *might* be important to the bounce. h2. Soft and Hard The general consensus is that if a *primary status code* is greater than 4 then the message is a *hard* bounce, anything else is a soft bounce. Of course there's no way this is an absolute truth, and I'm sure there will end up being various complex things people will want to do with a soft vs. a hard bounce. Since there's no way Lamson could know what you want to base your decisions to honor a soft vs. hard bounce, it simply lets you figure out which type it is, and then analyze the information it's collected to determine the bounce. h2. Lamson's Simplification Lamson already converts the email into a normalized form, so the difficult part of parsing out the bounce information is simply trolling through the parts and finding all these randomly dispersed bounce headers. Then it's a matter of parsing the values into something that can be use for analysis in code. To simplify this, Lamson uses a dict of headers and matching regexes to search for in the email:
BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': 
        re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}
Each of these headers shows up about once, maybe more in the parts of a message that has bounced information, and they don't show up at all or rarely in regular messages. What Lamson does is just walk through each part, finds the matches, parses with the regex, and then produces something like this:
{'Action': [(u'failed',)],
 'Content-Description': [(u'Notification',),
                         (u'Undelivered Message',),
                         (u'Delivery report',)],
 'Diagnostic-Code': [(u'smtp',
                  u'550-5.1.1',
                  u"The email account that you tried to reach does...")],
 'Final-Recipient': [(u'rfc822',
              u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')],
 'Received': [(u'by mail.zedshaw.com ...',)],
 'Remote-Mta': [(u'dns', u'gmail-smtp-in.l.google.com')],
 'Reporting-Mta': [(u'dns', u'mail.zedshaw.com')],
 'Status': [(u'5', u'1', u'1')]}
I've abbreviated some of the messages since they get stupidly long, but this shows you the information you get now. Lamson also provides a nice API through the lamson.bounce.BounceAnalyzer class that you'll see in a moment. bq. If you know of additional headers and formats that show up in bounces, let me know. h2. Bounce Probability Now, we've collected up all the different headers you might find in a bounce package, and we've attempted to parse the values into something meaningful. That means, the more of these headers that we find and successfully parse, the more likely the message is a bounce. Lamson keeps a score while it's finding the above headers, and for each one it finds and parses it adds a "point". It then produces a probability <= 1.0 that the message is a bounce or not. The more headers it finds, the closer to 1.0, the higher the chance. So far it looks like any message above 0.3 is most likely a bounce message, with only a few spams that are below that. Checking for a bounce then looks like this:
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
Which is from the test suite, and shows you various ways to get information about the probability of the message being a bounce. h2. Bounce Header Information Finally, you'll need to get at this parsed information to make any final decisions about the bounce. You might want to keep track of which MTAs consistently fail and include them in your black lists. You might want to tune your retries for different kind of soft bounces. Whatever you need to do, you have the original raw headers that Lamson found, but you also have a higher level API you can use:
if msg.is_bounce():
    print "-------"
    print "hard", msg.bounce.is_hard()
    print "soft", msg.bounce.is_soft()
    print 'score', msg.bounce.score
    print 'primary', msg.bounce.primary_status
    print 'secondary', msg.bounce.secondary_status
    print 'combined', msg.bounce.combined_status
    print 'remote_mta', msg.bounce.remote_mta
    print 'reporting_mta', msg.bounce.reporting_mta
    print 'final_recipient', msg.bounce.final_recipient
    print 'diagnostic_codes', msg.bounce.diagnostic_codes
    print 'action', msg.bounce.action
This is a simple sample that prints out the various bits of information available on a bounce message. Here's a sample of the output from the above:
hard True
soft False
score 1.0
primary (5, u'Permanent Failure')
secondary (1, u'Addressing Status')
combined (11, u'Bad destination mailbox address')
remote_mta gmail-smtp-in.l.google.com
reporting_mta mail.zedshaw.com
final_recipient asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
diagnostic_codes (u'550-5.1.1', u"The email account that you tried...")
action failed
This demonstrates how I've pulled out all of the error codes mentioned in RFC1893 and put them into Lamson so that you can get meaningful error messages for the various multi-level error codes you'll encounter. All of these error codes are available in the lamson.bounce module. h2. Comments Or Suggestions That's it for the review of Lamson's bounce detection. It's in the early stages but already showing a lot of promise. It turned out to be much easier than I anticipated, but only after doing the work in lamson.encoding to clean up emails. If you can think of situations that should be covered, and if you have samples of horrible bounce messages, then I'd love to have them. h2. Mailing Lists Coming Back Tomorrow I'll be using this to reimplement the mailing list sample and put it back into the bzr repository, then back online. I'll have a nice status update about that tomorrow as I finish it up. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-14.txt0000644000076500000240000000200611227105220023130 0ustar zedshawstaffTitle: Lamson 0.9.6 Sometime Today I think I'm at a point where bounce detection and handling works really well and is ready for release. I've got it running on the latest Lamson demo "librelist.com":http://librelist.com/ and it works. I'll be pushing out a new release of Libre List in a little while that does some advanced bounce handling, and if that all works, then I'll push out 0.9.6. This release will include bounce handling, the ability to change how state is stored and maintained, the ability to route on the envelope rather than the headers, and the full code to the librelist.com site including "Django Integration":http://dpaste.de/3qGB/ which really isn't "integration". The plans for the 0.9.7 release are to remove all dependencies on SQLAlchemy to make it clear that you don't have to use it, and any bug fixes. After that release, assuming there's nothing major I need to add, I'll be doing the 1.0 pre-releases. Shoot me email if you have questions, or hop on #lamson on irc.freenode.org to chat. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-19.txt0000644000076500000240000000446211230540446023154 0ustar zedshawstaffTitle: I Blame Bounces For The Delay I was going to release Lamson 0.9.6 much earlier than this, but as I worked on the bounce detection feature, I realized that if I just wrote that and a few other cleanups and features, I'd actually have a *1.0pre1* candidate on my hands instead of a 0.9.6 release. Instead of releasing 0.9.6, I continued to work on "librelist.com":http://librelist.com/ and got the software smart as hell. While working on librelist.com I managed to improve Lamson to the point where I'll be comfortable releasing a 1.0pre1 tomorrow. h2. What's librelist.com? If you go "librelist.com":http://librelist.com/ you can read about it, although the site is still rough and being written. It actually does function however, including basic archives. For people too lazy to click on the link: librelist.com is just a free mailing list service for open source projects, similar to freenode.org with IRC. The primary purpose is to give open source projects a mailing list they can use that has: * The code available (it's in the Lamson source). * No logins, signups, bundled services, captchas, tracking cookies, or other crap getting in your way. * No ads on your emails, ever. * Full access to your archives using rsync. * Easy and smart signup process. * Spam blocking and bounce protection galore (although spam is off right now). h2. Try The Lists I'll have an announcement tomorrow laying out the big things you'll get, but for now if you want to track Lamson and participate, as well as test the librelist.com code, then try subscribing to "lamson@librelist.com":mailto:lamson@librelist.com and saying hello. If you're interested in librelist.com development, then subscribe to "meta@librelist.com":mailto:meta@librelist.com and I'll be sending out announcements related to librelist there. h2. The Lamson 1.0pre1 Plan I haven't needed to touch Lamson much over the last few days, apart from some surgery on the Python Maildir class to add a safety feature. The code currently in bzr is also very stable and has been sped up a bit. I'm looking to basically spend tomorrow auditing the code and writing docs with the goal of pushing out a release in the evening. The goal then will be to make it solid and not add many more features so that I can get a 1.0 release out for real by the end of July. See you tomorrow! lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-07-20.txt0000644000076500000240000000754611230764565023164 0ustar zedshawstaffTitle: Lamson 1.0pre1 Released Tonight I'm releasing Lamson 1.0pre1 with all the latest improvements I've made while making "librelist.com":http://librelist.com/ and taking feedback from a few people using Lamson. The goal from now on will be to basically squash bugs and write docs, with only a rare feature or two as I find them needed on projects. h2. What's In This Release This release features really only four major enhancements, and a bunch of little fixes here and there. # There's now a working mailing list example in examples/librelist. # The router now uses the envelope header To (message.To) instead of the headers. # There is now very solid bounce detection and analysis. # Modules can specify anything as the state key (from, to+from, module+to, etc.) Here's the important changes from the change log by category: h3. Enhancements * 340 Added a version command finally, with some good information. * 330 There is now a safe=False option to lamson.queue.Queue to use a modified Maildir that md5 hashes the original hostname used. * 321 Routing it more deterministic and a bit quicker. It now uses the envelope To for routing. * 320 Refactored the routing to it is more deterministic and doesn't loop over the functions mulitple times. Still some work to do though. * 314 Bounce information now includes the original messages in easier to access forms. * 308 Got bounce handling working and tested. * 303 Slight reorg with the way @state_key_generator is implemented. * 302 Implemented a means to specify that a module will key off of different parts of a message. * 301 Got tired of not seeing errors during test runs so I made an option in the Router to explode on exceptions vs. log them. * 300 Bounce routing decorator for doing the actual routing of messages that are bounces. * 296 Implemented a bounce analysis method that is simple but seems to work on most bounce mail. h3. Bug Fixes * 329 Found an email that had a nasty header causing an edge case in the parser. * 325 Bounce detection was using the wrong email address. * 319 Removed the egg dependency on sqlalchemy. * 318 Moved the bounce stuff around in order to make it more advanced. * 307 Fixed up the @bounce_to decorator so that it returns the next state. * 294 Small bug in multipart handling where content types of 'message/' can be multipart too. h3. Librelist Related * 323 librelist.com sample is looking good, bounce handling is working, archives are started. * 322 Super bounce handling for librelist.com. * 306 Got the first cut of the whole app.handlers.admin working, including bounces, but Lamson bounce detection has a flaw. * 305 Got the basics of making mailing lists, managing subscribers. * 304 Initial commit of the librelist.com sample application. Also, librelist demonstrates how to integrate Lamson with "Django":http://www.djangoproject.com/ which turns out to just require a couple lines of code. I'll have some documentation on that soon. h3. Docs and Website Related * 317 Removed references to SQLAlchemy being needed in the docs. h2. Getting This Release As usual, try to install it using "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall and make sure you do the upgrade:
$ sudo easy_install -U lamson
This release does not depend on SQLAlchemy anymore, since you can really use Lamson with any data source you can access with Python. As mentioned above, the librelist sample uses Django models. If you want to download the source you can visit the "releases":/releases/ page and grab it from there. h2. Mailing List Back Online Finally, the lamson mailing list is now back online, so you can subscribe to it and get help. Read "the lists page":/lists/ to find out more. h2. IRC Channel I'm also more active in the IRC channel #lamson on irc.freenode.org. Come by if you need help. Thanks again folks, and shoot me any bugs you find. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-08-03.txt0000644000076500000240000003551011235577723023161 0ustar zedshawstaffTitle: Lamson 1.0pre2, HTML Email, Standalone Lamson 1.0pre2 features two features that might signal the end of the beginning or the beginning of the end, depending on your perspective: HTML Email and Lamson Standalone. HTML Email support comes from a new module "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html that gives a nice template method to send out HTML to victims...uh...customers. Lamson Standalone will be a way to run Lamson as your customized email server instead of another server like Postfix. Both of these features will be done for Lamson 1.0, but are currently just getting started as of 1.0pre2. Once they're done that will be the last two requested features people had before 1.0. h2. Getting This Release As usual, you can get this "release from /releases":/releases/ or if you are too lazy to read that page then do this:
$ sudo easy_install -U lamson
In addition, if you want play with the new features you'll need to install these (optional) Python libraries:
$ sudo easy_install -U pydns
$ sudo easy_install -U beautifulsoup
$ sudo easy_install -U markdown2
$ sudo easy_install -U clevercss
I'll decide if I should make these mandatory or optional with the next release based on people's feedback. h2. Lamson Standalone Quite a few people have asked for a way to install Lamson as their primary email server. Usually they want it for just their own email on a machine just for them where installing something like Postfix is overkill or just too hard. This is entirely possible with Lamson, but it's kind of not the right use for it since simple delivery of mail is much better handled and implemented by an older server like Postfix. Rather than resist giving people a way to setup Lamson as their primary mail server, I've decided to start making it possible for them. By the next release (1.0pre3) you'll be able to run Lamson as your only email server for small installations handling mail for one person. I'll also include a "screen cast":/videos/ showing people how to use the standalone functionality to build a *Personal Mail Management Server* to filter and control their mail without any other mail server. I'm going to treat this as a first project for most people wanting to get started using Lamson for their email. As of 1.0pre2, the functionality that's available is code in the "lamson.server.Relay":http://lamsonproject.org/docs/api/lamson.server.Relay-class.html that will use "PyDNS":http://pydns.sourceforge.net/ to query up the MX host for recipient addresses. It's fairly primitive right now, but you use it by creating a Relay with the host explicitly None:
relay = server.Relay(host=None, port=25, debug=1)
Once you do this the Relay will lookup the hosts rather than trying to send through a relay host. Don't go crazy with this yet, since it has to be tested with various kind of nasty email addressing out there, and it needs to have a way to generate a bounce when it has an error. h2. HTML Email Well, I broke down and implemented my idea for making HTML Email easy as hell to generate. In the past I didn't want to include simple HTML support because, well, HTML Email is annoying as hell and I didn't want to deal with the support headaches. I knew once I threw HTML generation into Lamson I'd have an army of marketing people using Lamson poorly to generate their marketing materials. I also think that HTML formatting in email doesn't work as a customer development strategy. Yet, every time I tell someone about Lamson, the very first, second, third, and 300th thing they ask is if it does HTML Email. Over and over and over this was the most important feature, above spam blocking, filtering, building applications, intelligent state management, or anything else that Lamson supports. Well, if the people want HTML email generation, then the people will get it. I introduce you to "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html which makes it trivial to produce HTML in your email and doing it in a nice clean way using "CleverCSS":http://sandbox.pocoo.org/clevercss/ and "Jinja2":http://jinja.pocoo.org/2/ templates. Let's start with the simplest little example that will send out a disgusting html template:
import sys
from lamson import html, server
from config import testing

relay = server.Relay(host=None, port=25, debug=1)
hs = html.HtmlMail("style.css", "html_test.html")

title = "Test Message HTML"

msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")

relay.deliver(msg)
You could run this right out of the Lamson source tree like this:
export PYTHONPATH=tests
python sender.py thedude@thedude.com victim@gmail.com
The result would look like this in victim's email: !html_email_in_gmail.png! h2. How lamson.html.HtmlMail Works The rationale behind the HtmlMail class is that you'll typically have a template you want to send, some CSS, and then body content that you'll plug into the template as a slug for each person. What HtmlMail does is let you setup the template as HTML with Jinja2, and then specify your CSS using CleverCSS (which is way easier). When you go to send, you generate a "Markdown":http://daringfireball.net/projects/markdown/ template as the body. This lets you write the actual body of your HTML mail the same way you would a regular email, but still let's you get a good HTML output using the HTML/CSS templates. With markdown regular folks can write the marketing copy for your spam while keeping the design separated. HtmlMail then glues this all together by doing the following: # Parses your CleverCSS template (after running it through Jinja2). # Runs your content markdown through Jinja2 and "markdown2":http://code.google.com/p/python-markdown2/ just like all your other Lamson templates. # Injects your CSS *into* your HTML tags so that you get your styles even though many clients rip out the *style* tags from your HTML. See the output sample below. # Knits your generated content into your HTML template as the {{content}} variable. The end result is that these two lines of code from the above sample:
...
hs = html.HtmlMail("style.css", "html_test.html")

...
msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")
Will let you blast out well formatted HTML emails that have a high probability of displaying in most mail clients. h2. How The HTML And CSS Looks Here's what each of the files in the above sample look like. First the @style.css@:
body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h2:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black
Notice that "CleverCSS":http://sandbox.pocoo.org/clevercss/ supports quite a few very cool features, like calling functions, nesting, variables, and calculations. Next you have the outer template @html_template.html@ that wraps your content markdown template. Notice that there's no *style* tag where the above CleverCSS is placed. That gets done later by lamson.html.HtmlMail.
<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h2 id="dull">All done.</h2>
    </body>
</html>
Finally, you have the @content.markdown@ file that has your basic markdown formatted email:
Hello
=====


I would *love* for you to tell me what is going on here joe.  NOW!


Alright
-------


This is the best I can come up with.

Zed
You can use any format you want by changing a setting when you construct your HtmlMail object, but markdown seems to be the closest to what people would send as plain text for their email. In fact, I'd like to find a way to send the raw markdown as the plaintext version of each HTML email that goes out. With the above three components, you then get the following output:
<html>
<head>
<title>Test Message HTML</title>
</head>
<body style="margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="background: black; color: white">Test Message HTML</h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h2 id="dull" style="background: gray; color: black">All done.</h2>
</body>
</html>
Like magic HtmlMail has taken your CleverCSS file and merged it into the tags of your HTML to style it, making it work in most clients without forcing you to do it manually. It does this by walking the CSS and generated HTML with "BeautifulSoup":http://www.crummy.com/software/BeautifulSoup/ and setting the *style* attribute as it goes. h2. Librelist JSON Archives There's been a ton of work on "librelist.com":http://librelist.com/ to make it work better, but the big feature is a complete JSON dump of the mail archives and a new "Archive Browser":http://librelist.com/browser/ for reading the archives. The browser is just a fast one day hack I did to prove that the JSON archive format was good. It uses "JQuery UI":http://jqueryui.com/ to build the interface, and has *no servers side software other than Nginx.* Yes, you read that right, there is no Django behind this, just a pure "Nginx":http://nginx.net/ setup serving files and directory indexes. The real meat of this setup is the Lamson server on librelist and an Nginx module. First, the librelist server generates a JSON version of each archived email which you "can see here":http://librelist.com/archives/lamson/2009/07/30/json/1248947932.M724307P15430Q6.09c5769d5b9f3d575cefc2ccb51877ec.json with your browser. This JSON actually loads as an object in JavaScript, so you have code like this:
function appendMessage(msg) {
    display = '<div id="message"><div id="header">' +
        '<div>' + msg.headers['Date'] + '</div>' +
        '<div id="addressing">' + msg.headers['From'] + '</div>' +
        '<div id="subject">' + msg.headers['Subject'] + '</div>' +
        '</div><div id="body"><pre>' +
        summarize(msg) +
        '</pre></div></div>'
        
        $("#messages").append(display);
}
Second, there's a tiny modification to Nginx's "autoindex":http://wiki.nginx.org/NginxHttpAutoindexModule I made so that it will generate a "JSON version of the index instead of HTML":http://librelist.com/archives/lamson/2009/07/ so that you can "browse" the stored JSON files via JavaScript:
function loadDay(date) {
    if(window.MAILING_LIST) {
        $.getJSON("/archives/" + window.MAILING_LIST + date + "/json/", {},
        function(data) {
            $("#messages").empty();
            base = data[0]
            msgs = data[1]
            for(i in msgs) {
                fetchMessage(base + msgs[i])
            }
        });
    } else {
        $("#messages").html("<h2>Now pick a list to look at for that day.</h2>");
    }
}
With those two components I can export a JSON version of the mailing list archives without having to run a large web framework. h2. ChangeLog For those who are interested, here's the Bazaar logs from this release, separated into changes to Lamson and changes to the included librelist.com sample. h2. Lamson Changes * Cleanup and tuning of the html code. * Documenting the HtmlMail API. * New HTML email generation with CleverCSS support and merging the CSS into the HTML results. * Additional tests for the new MX query style of Relay. * Implemented a feature to let the relay do its own delivery, rather than needing a smart host. Requires PyDNS. * Cleaned up the build so that it removes junk from the examples. * Fixed up the lamson default help command so that it is more succinct. * Properly skips spam filtering if the spam databse doesn't exist. * Made the spam filter not barf if there's no spam db, which helps when testing. * Now don't bother storing the START state since it's the default. * Cleaned up mail some more and deprecated the MailRequest.msg and MailResponse.msg to be replaced with .base (since that's what it is). * Added a simple method to copy the attachments of a MailRequest to a MailResponse. * Got decent attachment copying implemented, mail api needs a bit of cleanup. * Return an empty string when trying to encode a none. * Minor bug in the safequeue, added logging so you can track oversize. * Getting the headers right for replies to work, removed the first message. * Added options to Queue and QueueReceiver to limit the size of incoming mail. * Added a version command finally, with some good information. h2. librelist.com Changes * Force the permissions to be correct for serving up the archives. * Make sure that bounces are always saved for later. * Json convert script can't go there. * Need to install simplejson to make the json work on librelist. * Prevent loop back of messages from lamson to itself, and stop messages that should not be considered lists like unbounce and noreply. * Implements a json version of the mailing list archives in addition to the regular MIME encoded ones. * Update of changes from the live server for setting up the spam system. * Migration to install the spam database on deployment. * Added spam filtering to the admin handler where it counts. * Confirmations are removed after they are verified. * Template for bad list name help. * Tweaked the formatting for the bad list name helper, and made sure that it returns to START. * Now giving out an error message for people who get the name format wrong. * Added additional headers needed for achives, and try to maintain the date and message-id headers. * Production and staging deployment scripts setup and a migration to make those work. * Implemented a simple bash based deployment setup for automating librelist deploys and migrations. * Nope, can't add the - yet without some regex wizardry. * Small change to allow - in the mailing list names. * Updated the messages librelist sends out for readability. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-08-22.txt0000644000076500000240000001221711243717140023145 0ustar zedshawstaffTitle: Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage I happy to announce probably one of the last few releases before I officially put the 1.0 stamp on Lamson. This last 1% of the things I want to do takes a while, but it really puts a good shine on the project. What I've got in 1.0pre4 is "tons of docs":http://lamsonproject.org/docs/ and 100% test coverage. Most of the features that were added are nice-to-haves that I've found useful while developing my various sites. h2. Getting The Release As usual, @sudo easy_install lamson@ is your friend. If you want to use the HTML generation features, then you'll want to also install:
BeautifulSoup
CleverCSS
markdown2
For completeness, here's the remaining packages I have installed in most of the virtualenvs for my applications:
ipython
mock
nose
Jinja2
lockfile
pydns
spambayes
chardet
lxml
python_daemon
pytyrant
You can also grab "source releases":/releases/ and "download instructions":/download.html from this site if PyPI is failing you. h2. 100% Test Coverage First, I've managed to get the test coverage for Lamson up to 100%:
Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               124    124   100%   
lamson.bounce              91     91   100%   
lamson.commands           176    176   100%   
lamson.confirm             57     57   100%   
lamson.encoding           238    238   100%   
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.html                62     62   100%   
lamson.mail               140    140   100%   
lamson.queue               74     74   100%   
lamson.routing            218    218   100%   
lamson.server              80     80   100%   
lamson.spam                80     80   100%   
lamson.utils               56     56   100%   
lamson.version              1      1   100%   
lamson.view                22     22   100%   
-----------------------------------------------------
TOTAL                    1429   1429   100%   
-----------------------------------------------------
Ran 156 tests in 16.305s
It's mostly a vanity thing, but doing this did uncover a couple of minor little bugs here and there, and it makes people feel better trusting Lamson. h2. Docs, Docs, Docs I firmly believe that a good software project has *both* "API":http://lamsonproject.org/docs/api/lamson-module.html style documentation and guided documentation. That's why I spent almost this whole time working on documenting all the parts of Lamson people need and making sure the generated docs were complete. What I've done is reorganized the documentation section so that you can find topic by their categories, and then documented most of the features people need to use daily. Here's some highlights: * "Unit Testing":http://lamsonproject.org/docs/unit_testing.html * "Confirmations":http://lamsonproject.org/docs/confirmations.html * "Filtering Spam":http://lamsonproject.org/docs/filtering_spam.html * "Bounce Detection":http://lamsonproject.org/docs/bounce_detection.html * "HTML Email Generation":http://lamsonproject.org/docs/html_email_generation.html * "Unicode Encoding/Decoding":http://lamsonproject.org/docs/unicode_encoding_and_decoding.html * "Hooking Into Django":http://lamsonproject.org/docs/hooking_into_django.html * "Tons more...":http://lamsonproject.org/docs/ I especially like how the HTML Email generation came out as a feature, but I really like the "Bounce Detection":http://lamsonproject.org/docs/bounce_detection.html documentation the most. h2. New Confirmation API This release has a few little bug fixes, but mostly it has a solidified feature for doing "Confirmations":http://lamsonproject.org/docs/confirmations.html easily. h2. HTML/Text Dual Email Previously the "HTML Email Generation":http://lamsonproject.org/docs/html_email_generation.html feature could only generate HTML email reliably, but I managed to track down a bug that lets you now craft dual email. I even went a step further and made it easy to simply use your markdown content templates as your text/plain alternative when you send out HTML Email. h2. Multiple Recipient Routing There was no way to avoid this, so I just implemented it. Now when a message is for multiple recipients (in the envelope) Lamson will route the message to each address. That means if it is destined for multiple people, then Lamson will effectively try to match each of those people and run them through your handlers separately. I was worried this would cause problems, but so far it's working fine. If you see that it can be used as a DOS attack, then I'll work on a way to limit the number of recipients that Lamson will allow (similar to how you can restrict the size of messages off a Queue). h2. Test And Report As usual, please test this out, review the documentation, and report back to me any problems you find. I'll be in #lamson on irc.freenode.org more, and will probably install a IRC bot to notify when I post new code to the repository. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-09-07.txt0000644000076500000240000000241711251323145023147 0ustar zedshawstaffTitle: Lamson 1.0pre5 Out I just pushed Lamson 1.0pre5 to PyPI for your enjoyment: "PyPI Download":http://pypi.python.org/pypi?:action=files&name=lamson&version=1.0pre5 You can also get it from "releases":/releases/ or by doing the usual easy_install. This is a small bugfix release that only adds one small feature. If you're running with the SMTPReceiver, you can raise a server.SMTPError with an error code you want and the SMTPReceiver will return that as the error code to the client. For example, if you do this in your handler:
@route(".+")
def START(message):
    raise server.SMTPError(550)
Then it will generate a reasonable error message (using lamson.bounce messages) and give that to the client. You can also give a second parameter to SMTPError that will be the error message if you don't like the automatic ones. Only warning with this is use it sparingly, as it then ties you always running Lamson as an SMTPReceiver, instead of off the QueueReceiver. It also leaks out to malicious clients more information about your server than you might want them to know. In general, it's better to just ignore bad messages and save them in an undeliverable queue for later inspection. Alright, try it out and report bugs to me. Thanks. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-09-26.txt0000644000076500000240000000226411257445456023170 0ustar zedshawstaffTitle: Quick Lamson 1.0pre6 Release, More This Weekend I put up a quick release of Lamson that fixes a bug in the gen project prototype. I recently switched all the domain names being used in the test suite to "localhost" or a similar nonexistant domain name. Why? Well, because some people liked to run the Lamson test suite or their fresh project test suite against a *live* SMTP server. The test suite would "send" mail to test@test.com and my own address, under the assumption that people would use the @lamson log@ test server. End result: Periodically I'd get bounce messages from test.com and other test suite mail. Well, I made that change, but forgot to update a couple places in the prototype, so newbies were seeing the tests for their fresh project fail and wondering if Lamson was broken. Sorry about that. You can grab this 1.0pre6 release from the "releases":/releases/ area, and you can also do the usual @easy_install -U lamson@ to get it. I'll be doing some more work to fix a few bugs this weekend and working on a fun new site for guitarists, so stay tuned. And if there's something in particular you want in Lamson just email the lamson@librelist.com mailing list. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-12-12.txt0000644000076500000240000000701111311017422023123 0ustar zedshawstaffTitle: Lamson 1.0pre9 Is Out *UPDATE*: This used to be an announce for 1.0pre8, but python-daemon apparently has a bug that makes it not honor the TERM signal (still) so I had to go back to HUP. I just got done knocking down all the bugs in the new "Fossil":http://fossil-scm.org/ based "Lamson Support Site":http://support.lamsonproject.org/ and cooked up 1.0pre9. This has a ton of fixes, but I'll let the ChangeLog speak for itself:
=== 2009-12-12 ===
22:49:43 [c2be9dedc3] *CURRENT* Version bump to 1.0pre9 thanks to python-
         daemon. (user: zedshaw tags: trunk, 1.0pre9)
22:43:04 [1718d014a5] Stupid fucking python-daemon doesn't recognize TERM yet.
         (user: zedshaw tags: trunk)
20:24:20 [b5796e14a1] Pointed at the tag instead of just the leaves. (user:
         zedshaw tags: trunk)
20:00:38 [42768ade2b] 1.0pre8 release announced and done. (user: zedshaw tags:
         trunk, 1.0pre8)
19:43:53 [bb1b0a0dae] Updated MailBase.__nonzero__ to include parts as well.
         (user: zedshaw tags: trunk)
19:40:51 [2916b83307] Created ChangeLog from fossil timeline of commits.
         (user: zedshaw tags: trunk)
19:36:14 [41530d8a34] Version bump to 1.0pre8 for release. (user: zedshaw
         tags: trunk)
19:31:21 [dfe158b282] Initial support of using Delivered-To or similar more
         exact To header. (user: zedshaw tags: trunk)
19:14:13 [e9988679b7] Updated prototype.zip for upcoming release. (user:
         zedshaw tags: trunk)
19:13:51 [98e94b6103] Potentially fixed the unicode/ascii issues with shelve
         storage of confirmations and routing. (user: zedshaw tags: trunk)
18:56:35 [6224ae68e4] Updated docs about confirmation to fix a doc bug. (user:
         zedshaw tags: trunk)
18:56:14 [103454c43c] Make things clean up better on INT or TERM (not HUP).
         (user: zedshaw tags: trunk)
18:25:29 [009451cabc] Added meta tags to the site so it can be found easier. I
         didn't know search engines still use that crap. (user: zedshaw tags:
         trunk)
18:22:24 [8908d7852d] Applied starttls, username, password patch to server.
         (user: zedshaw tags: trunk)
18:16:01 [9a9f9f37b6] Applied patch for handling odd email addresses and white-
         listing what headers have email addresses. (user: zedshaw tags: trunk)
18:06:06 [7605ef0517] Patch for queue bugs applied [bug 9d330c8f80] (user:
         zedshaw tags: trunk)
18:02:50 [ef9808c4f7] Point at the right archive browser. (user: zedshaw tags:
         trunk)
18:01:39 [3b3dc9391a] Better logging message on failure to connect to relay.
         (user: zedshaw tags: trunk)
=== 2009-12-05 ===
18:05:40 [ddc4369b1d] Initial commit to the new fossil based support site.
         (user: zedshaw tags: trunk)
17:48:22 [8140fcad03] initial empty check-in (user: lamson tags: trunk)
This closed out a bunch of bugs, so if you entered a bug in then go make sure that it's actually working for you. h2. Getting This Release As usual, you should be able to install it with PIP or easy_install, and you can get the raw downloads from the "releases":/releases/ section of the site. h2. Submitting Bugs Keep in mind that this isn't as heavily tested as other releases, so if you hit a bug, "report it":http://support.lamsonproject.org/ and I'll fix it. Even better, if you supply a patch then I'll consider giving you commit rights. h2. Potential Contributors Most of the changes were from supplied patches, and if you supplied a patch and want to contribute to Lamson, then contact me and I'll give you an account on the site so you can commit directly. lamson-1.0pre11/doc/lamsonproject.org/input/blog/2009-12-13.txt0000644000076500000240000000040511311261207023126 0ustar zedshawstaffTitle: Small Lamson 1.0pre10 Update There was a very minor one line fix that corrects a fairly obnoxious bug in Lamson due to my not understanding how __nonzero__ works. You'll want to update to the latest "release":/releases/ if you are following Lamson. lamson-1.0pre11/doc/lamsonproject.org/input/blog/._html_email_in_gmail.png0000644000076500000240000000050511235004025026226 0ustar zedshawstaffMac OS X  2EATTR}E]**com.apple.metadata:kMDItemIsScreenCapture3,com.apple.metadata:kMDItemScreenCaptureTypebplist00  bplist00Yselectionlamson-1.0pre11/doc/lamsonproject.org/input/blog/html_email_in_gmail.png0000644000076500000240000012752011235004025026020 0ustar zedshawstaffPNG  IHDR3 Bb$iCCPICC ProfilexX 8?3c0jY6uZ2ƒRH("JZD=$Z*HH!d?C}}]߹ywy}~H~ti-@͍K"#zĘaؒ.q |㟌y?(s@҈k0e0z x30G,M!"8^7p#olyZ`22LCY`=?>KD.I!e #euαdAS#S`a :ج)/nbipmmFyԵܵ<PCh\F6=@!ax(a.:9՚m7W/7ͯ#&r8 5Rreq{N-+x+v+䩡]5jt= Ffi&fZ6V 2lWD'}~HqSogd^ F ARZ,=?rrQcrz NhĪ)>% (;{+9 ]MunԅGF.O}+jϵYrfrWs)w8vpABaΝw%rFe]+="=46z[X\-:gz[6ldkbiZ~~oh1iOWgzC_1|]@ {"C7F>?-~b; FoY-_"ts7oMSS}A01813?2;\|ӂB/_3KRԲJתjÚ5$CI" B2qa̯YOrDr*sl%HT_niA*R챔;/?(DU.WYPSU(֜Ж9WG676nogma@[BVi 14 ٽurlsjW@s. T7KdNG3gGЄo7/?% *?ȝlN7 c 2:2pґ([OxS{/S 夡ӭg%צ<< \ eKӪ.=Mox}ӕlDD5])po}ۢ]{ՊJKU4u+ XV:T?z\ɣڎuտy񢭱eg_W}.tw޿P=>Lh:"SWOCS0 X J㣵5!Aaa?K#B#U#79ewt/NJÎOɍ 38;9_*3!,>I4ҙg)gSS'^8u*"+2o\ͻ}9ZnV^7n?\[=-ERŚ%6e'*|~V( K-ilzsRCƋ&6LEDžnB޼/,`8O_F'&T-t-]^Ue#1rF K8w$$C([Um$.a $6@4A5'CHP UBm0s63pAvH0T6 &i6&4S}IfY9yň%k$k VmbóEaOf(Jf>ySfix޻ķǖD~6me.5ۃ%$aI"]b$K K/$;0B/93y9mRrJj 5O&%_04iTbĤô%ʊo-f#WdHu ?@sj6nd61 3«{7Z7Rȏ\u!+MRFעre BW$޹yoxwilJC5xPM[Z :GzM <=9]d% %p<}` BC8Ht: Kh`6)q*\(a1*uՁF5 ,a\:W=[/M 7Z'tw6x}~gvO Z M3w؈ʉ}L)u` nwcrG+)+)Q9qM3_RACE&f3˭Vц76}CL\q.xWq]:d=3d|E~#!1l);6%K9%f_lfpDtR)ŜhV.qj&jOF\oojʹZp>eko*دֵnKz={UsC#"_㨉酟 ˛ E "0$)A3 JJV3 B G'jjEs?31gĈb"1qK֭q?dK6c%{898 ]yp; Z]$b[xTa#Gm8kʽKX]*+YVO֨>~)ٹW |/\s>HW;MҺ?$V7 =?z0qϻ荚Y[0T&>1{ϏYEs 79,b˖VV VFDߨ`ޜ>QClmBVHt-!нHύCܞ>&1MQo rHo%lI3DrCrCKx ]?4NF,* 4B19 uMamc\ ~%jhqZЪCo2h ZVGT'Lq>Y |1wd9ֶ>F#=Ā7{Aƛ x9YYU/}2ڿ pHYs   IDATx \OiS)$*nFC~hl50 aFScma1&1Yfd B 7** ۽?޵ds>yys==wd_?򔡦wm*f:ebϫ;B4ΝÇ=ux-/^=zt}}z4x\gkh51LMd;, 4KzP {یMFH $I=7_*$@H]$IMM)5fˎVzmż )ܽo{+*k@nJmmTWɽ'ٯc.H $xԻC[SRGnNt]3CsE’' o{Cy<&=a7\Y6FܝjnU-Iӻڠg"$@{f,M-BۭoJOK3r6&T$_*Xu'Ov[]m+1'"m4 U۩1"'Ok]ɃQO{*V46 "䉐[T@fӤI[mEzai#Z_J,_.h׻~ ne>2勞`ĖkrT\SCg+3E9b5 Mͻv2V;\Cf:^[z̓|K2[5\Ŕf̋UD'G{#кbMFB&V}O&{)"z$VJyUkdEyCH#̼U}L{P_) l\:6hi+jQeBEί,z|Dv+wR΍U{,-)#݆EQ!gbvU{cfx!M6NYt`'YsC@32f 57%?Sޤ.Q7(}hGI{8YʧmYh{*`3]/|e^L![6I n0Q͂mV?aؘtvŀ&dY5ؘpPss 'ʊ 9f_Ù_F;>/I-P#iLi޾?59Wl(\1P&,O,r]&z%# ]W\w(] R¦Oo0lZS)AKUoF{cQ.0HUHkn; O(3N?X,]ikj)]<}R̊ku5X M@:X8k*NE&}2F݄~֝8@#?fHrzuhG37n카[6ҵHsRUT`oR![f%RAK5g;Ve;(Ҿ8K-R]zå nʚAͪ B,~8xk )h H?9xrTb"K(T_Jfh[Fj …;b:FF ה=xPʘBWBrSHGMBa"1@8@Bߊkׄ/|#e=]r Pz쾞;dbm Pi܉M{@o11 3T:[T .8'N697*<86Oߚ#ᑛ{N\~K޻\>t$Gמ 3 ?xUm^ԍ<{ mGJXr-M7H܃]B\V >4L/v4yʚIz[\JBeJ0}u%fcN;v=a4'Z-t259#u,B-?svqh H!WÎ*O|F.Xֶ&+N>}ܹs36w,{}jXTzu{ &~I՜y{>z坘5+͠ʏ,3cJwQ#SRydo3Z9g\/Jp++P3쾰I} 6{:M~v\2(HXI>oc[L҉VK4ݱÉۧ4fIu ̻/)\99o>,wDV:G]v+w?:o7;hl7TR=z arY>>s|}ImCٝJ'-YV'1ߧujkD_| F@uhI/~jr#:#É'_ c?Zw`& 6~tɧ#>S{gDHjAФjaRe?fɄ7oSMN$\fa&sW}س+K%,דnZe$~mdYnn8sRM c/KguI|,f|T1wi:a 5R,--]b (DNѱ EՊ>Ւz~]!L apn_.; YJ.)!Uֲ)BDw=5SOJ';xt3njoX^״Zu3 $i 0 ͯXfoV@*F|*P'YsY>H%RMEOJxyam2THAb4U#P[ j\ %y*Z-|JQJUfYcOjl&'4g+?nzܻѰ#E枾x] p!;nrN`K8%=U^OH0K $B#GIcFջw[n1&L`kkZFFMnChL]?՟zC9@"%ϼZzTNl#}qРIvadºgʦVW]`<;J?zЄi*.<kzRzXFN[l >RgwMq G)d@bk Qح׃:}ޓ^m2ݗ#H'3#m}-wNYVq0K& mpn߳9Ziҋ[D˶Lp^.+qDOHA$VQ򵠪Bڬ`4A|2d媕C\ 1Zv75-ד;Y]zӯW}ř"1lPOe3&nQ QGԒeW{sF`uz9JsZi/UfNҲuQ֐H(r;rj S7,}ԄJaiSOy|ԵNe足2 >zym=CnBXd9b-#1~{W׀u bʷsfP3E'5]`!EYbPP/YS&IXE>-M;g݌Q'~ 3JWw|y޿l`N%Yj#l_G+:C4nu!T* z1ԗ'hL Fm4V#XC?[7{qdY#l97"s\|tnP S`Ye.&O:fdMs{,0bݧ3LWK:0L[$k'6>f)5){ U_MUHڹ]6 #pi# aN͈NT8RuÁ$&,樓VCmt$Y \h)8x4ۯ>ܧNzYnEW`I3khтFw3g2vn3'B>}Hnz\M,u c͢>kA,O_;I's`aƼ}Hq_@w߱)Wv]BK\yLI-m J~>2uؾ^y2t jhLkϰ{;TO?n_/QTu(T<6qFV֭[YY׎L%Rྛ\>@"j5-?FGk앧c<.t+`vO1cP]X26S۵O?xEL:0UQG :WƑ?ߞal@N]/"H&Nz[+֋BsNmR[Ls`tRk]:TN Ӯi))ʻSp7qHUGEꟐB* 1EyOTR6Ea_q(dngΥ~V|? jUqG 5!~ܳIHEУ:[x˧$H{JfȯwkkSDƵRW#FpttǍ׭[7iyۖK;O<\Dy9u~q7K`t!p2`OJtҿgku'ppovLWKFfi3Ժ呖zkIɶ'H(}rG$`&MvMAìý'=[+}ӈPe'*/uZoIiY* tk,LDb(G wz+|-l4ǜg֦=3+Sۜ$VQR0qe-Oj z=78 ay{f S~(y 4<PQ?|LnGͨ#𙥃Ԛp(zUsxiԾĹd"1ⳍk afړ<_ dhm;j`ԩ#!Lu g`ay0UHe:zuϴS9 7u#]fdj ~eZ 96Veb}l(+ qm>hp'üe}w2ǭV;/ZgfvCG\&RE jj(M=ϏVW1T%u{0qHVsIi,mqk?q3?uq)FA@%k@ި~FɃDyB궞2n~a  PxuY% =|x4WO~o/_>o޼e˖5H]%8[~N{+t9+W@LAe|ޔq능0ء,zT_V CX$Y$?/lEms̝nL5|d$I2蚮CS~=MՋOۚFÐƝwN7x h?~ŪLD o<}}+7BX;zzN}2f#b!ou&ݲѣG5觾+џˌoɪ>Q+VB!b!I:yP2ZNZZ-7b@!deQXeFZ6!u,>azVXHnhsV aX:diz d:Qm3_ٯ5TmP" U=j#-QWovH*iH Pα 2:8Ūؗa~+W"JnTCIm TNeђD퍲$n d)v>NZh@-*i?dvI5TjJI**+nʂ JO/@1-VD/dBaa$@zfhSj*C@H Jw3_!X@H $y虽C $@o f1e;toh@qh[Byu<xGgt46 $x ̷D$@H!;L$@H- [Ih"@H $@hl&@H $@-$4 $xGgt46 $x h<{-0MDH $;@ށN&"$@o ޒB3@H wzf@'c@H zfoIGH $;@=wH $[B=L$@H ;D$@H-![Qh&@H $@dl"@H $@-(4 $xgt26 $xKgt@H 3{:@H %3{K: DH $Mxf>ϮVeL5,:"D>_0J1Y]l[AӀ)F ҘnNqY#].w?]"fKNK͂w gg w("e75|BH $j 4ᙉsͩS>$)Laζ[s\uEHHVu˘^d76"Kd"==⊠J9J;uMN'%d^C_~-5rP $),5J~hifN[x>z= )Hl uκ$r/Av|<)Kycu{lxzT/i\_Ѹ7l"⛉ts0@H *M0pHWg~#Iv7tݙ<s+=|&CCg={n9'&KFX3u6ϟRs&> ))9;Q4yy %+NHo::]&]-q8a42|Vh$'uy{%ϞQ˔j P%A._䋒434U.i[KLN8FbɁ@hDGhr(I;4gg̗SsߌYwqzSu(OgR .K+TfH $^*&qqv*sJ3Kn2GFVzu4fu1wYsMgs CtºYi~Xǧ~5$2D"RYr.&hq2OH=M]E^/!ޡDwj,ϓCfSwx 766I}" $`FC]Qm/uaN8e!\!_ȉLqUZK|r3~Hfæ[̞)3l$@@ #%D"#b2.Uޒ ]@pbC8C~]} yg+ A%FGl;6@:memD6e}Z_:ZA36NWb(J;06edR´GZ B)/̥MM$֔J;MP9D/mK*啫؟(NҥtW/&?ix(&y u]g^[O290'bYIO~"Rn-/7X~>kR>-)2H $^xf:X?#,}}-= ]8WO$#byDr_PԢ6tFaВTeMebu $1nΎ3֔\P1:!lOusB<9 f2ཟx5aGƱ(T!&)[>d7EռTI^@+ 6бC ښ޲f=V}0;}aPZ6}'qKn6՚=^PwiÝck8✈ ظf%@HhL>ㄯ<=r&?=ޠZ돛8果ۂe/)>=D+ ,g8|PYg{lR٭ ̝X)}uX@BZ̤˨K%|̴-WGݼ7H_ 87$5cgtZr'RDDq (dw4ЅO4iWΧBR›U(yTBِ0bd\~mMލм# K8/ҫk;Aa }'7[gO.쓥^psF`>@H &3#Wlۉ! Nbbޝ ]ږ8OqG99vdMbYȟ+UMXGe[b_9M$:vo}] Xp0Ś eتfX렫>Z*?mܥ 8|J@XVHL\jm=7Djl)Q$"Q,@mxYC|xLz7PԮ ǟ=h6'a;}s>dE"7D_>E!hlG,? lf"$xZ+ ޲fWe. ~1Qj*)"UYi*MoSV0_# d96)XHghh-3b>eg@Y[` 7O@7EGS/uR\Z#P.pdH?W8N~Fԇ<*Ci؄ g43_WU!J1;h^IAA`iF_h0AŘ@HЀO *,qG}el KPY#,c~5Cf BIcY"yu)oU3żJb+K[KTd?QΜe3Ѧ%ӝ9c\<n K%%d$X^vZy0n0J"$x^3{:iSt%oPyG&R$oaٿ+!H $g>Z߫l=? B}70!SxYYY$@H :虽:ϯY\W /!hjZZY͵g%@H 3{{mGH $]ZՌl @H $虽!f $@Hg $@o ޔ@;@H $$@H )3{Sz@H $zf8@H $@M  $@@H $BCΝ;1AH $h9ѣG7ngb|h]$@H 4MÇ7-V^x\Thkj OQ@H $T`\Uc930 $@՞YUUU fO>͠Ó'OJ$@H []}AT{fͶϞ=14+H $h9ovlL5,>zdjjjnf#Pʲ#N+]~h3%/sQ&+=1-AV[v5F5Ln'%O.nXMTV cThx=3.}U'oԷiUڪե} &r*RVP^|?=%) I)ijJ"#ZTJ\v0`Z|J8xY&XBVb" 昜/=`\]SibP-(UPSC $',{ b6$#KQy v/qs8$:{5c9*++km[{10;S_{WS񥾱? ?grY>;?={ƃŒ|d)ϩIQ9#7Ǯ ?Kiqq|]Mohx <97;!xwd[9HM._ނEg5;sV]2aIsfZQI3[V}qqEfmo̙_GDy8-;vW&&z;2`|ؾ;uIK5yz+xrH] n󥥰tC,?kDs\\2=c?\tmi]ސ $hţU CPr!.Шu(eۺ)T+tiU%w/[> 0$61/{bhbJ^w;¹I65W)>j*_eg ]0\`%tT4ʅl)DzT "$r5gt|³?=>bc:(o/O;TS)H~~v“֦˻N*>;c{CiݫN֤NGq ӥ `Dል7 ց?ɽ3zߨ!*5pI j-JeJ)$%$喱$AM8[b#K#\cde Qu) "{w&a_Jqs#;e0eWc@ R JcEUƸxq d6[o{3pY"zkDԔR !~^}-/sT\b8{_ₓ Ma!{}In&)hd;o"L?Vu! 2:r*HG|fiN<8c%. #m+$6&XODr=DDu/'.#z36q!EѲc_%s` 0떥t]Gr2-/b5$Z ,{`vDʽ"]L}z(sg缃.u09{ﰾڙx,>Of=N4q+SfǰZKbN+?v2Y2-#]AF(( :d@ N=31kլ4ʻF8ݨ"Y>+rwxX_3g&UWEbNPZvcx 'p2ېm3>]_ Mn;7QtK6Voq21:fep_}e˙IEϑۼo[wgӇۀ 1. r,ns>Զ 8plA :wi(,`۹~&"v6z-y\+c^P/'[YM$p6sbQoJ̐Cׅ?H <]}pl~:>}'40j=ڭSO{hu+Fk5+,\6,vM-&uꪧm6|=>אNRb8M\ͿF 1okҷya+V FO~x-7D{c]a ֕cIO^JIulIo, O/X}t0!kK﹤yysS,3IK)S˿H<;KΌ"+fTb dl1MlY`5+\y0&Vp;tչsGx ݳL;εkiN?BN) 23su6?*.'FvR׉HaIXLby dw^7vI;syLy[S~ cmVnNuI|lRuu`O=,udEE%|y!!Gg\M4X˽;ʣ:x wāl<[\0/#KA(cڅڲpM,%ibVW7R"sf6^p gHc6ϘuİcyosV?M!I9=)qq4߻A6MfY3rLv]]]Ph }ȌIׇ4j UB£wn?SSΈvtoo3zq~[tJ;0@Pnj2h䚸%R}CD`Om&U>3ooLTz!MruXzB tG˩#?jb.ݻv"U/VN³ 2ZUD8ؖ_At]g $QӒXy+sT1Icz n4 P[m'RUE<Ǭ^?lc&xNA SMX^Y6n~ԙmٴXutl&ma ړL_M]B$Q̊6WLAe/6%/D;b7Z*H` " L[7a_pt&5K9 \ 53#,&̐L,ģ!FW^_u|4c"8kK>VOuoԵ.ly\::SuSA`ne 6M>%eUpe\r]+uJb'پ/j9 "b01;w!Ƃ_<eyZ*34Zx&r3E:UDӴIMbٛKto*ȖGjÖ9w\ȳځޫe鵵ugKζI}vDy(QY~sk +k+ Zz ʒ= wEL )?3oeb\gڷOCʗWT폷0 $Ј@}[SqaZ']GC?\U!,bwG:wiO]joN7q.Yaŋ?ݸW-|#z\']ص_ X{{iZiڪ_kd:д&KcKxY@ Sk*Rʔᕦ#͌ͨ`l6l6I*_6Hdgr īF9H@ެcpOP$v,.N0#"MzL"&2x5n@yobr6PxAs_JT K%ܪR=P.kn.LoOp 8kTkԙ8}R}r|ؽ'#ǭ]:y{0]P%CXw;H .ʕ>6τ}QTA(r۵k' kxښ|a}1|L eƝJV%' z |ffo77ycb<$G7H`8(s/;Grx:~4ML} .HFgL+K)p[goXpۗ.=#ćBm}*Ys=IS: UɆ}\,qrqF(# z}3/yKOs|wVr|(.Fx+CPF6 m-MQ ތ:J_Ǫ?mo@Yٸboˌ"n9s݈ٸsK!9!(/WL.NP ovĂQwև  ^><빬$F[R&۸|.~tC1SI(j1&/YI% &g.u07g{Ro*u$KOi΄z:xxdž`G&x}4S>4u/w4`˜OUs DžB5-X}1ԙK1#;*$fug˸]TEg6hc7}sPw(]u;]yP9؜]gC{ O 2kXBM.[+ jeb?7]vmъYFVѶmeeP\Tdbj ~9s-XEԧT~sKSXGv5H>=3SI#z˓s]1CL$gJmav]=5?FHgpQꕶB6{fFPM{fŚC?lKAY =1|֎H#@}T ֫Z𹦖K6HW>?T ?& $OEtZkNiƪ}uX"tPg~mV*2_vdŰl"ѳr0&)[{l[VOGIXٗuԥ ʔ&院g5̲GiE[kj־j-/(}4u P+$Z[ܗ>RQQ, ߛli ~ɊKWDtt?:X V=8*v6 }W5H z}v7Z73$Hsylɯ7{Ĺ5mPwߥ!vp~jk)۬a~ M9^2~8+?++dh"E XFo>)ȫa׷hj !ogDߕ;)YUrNG; oUZٻBۉ@M73 YzxJ=:.=}6fegK:L4=?V@H 5AQv7^0 $@HhctT@H K Slhܫkzf@H $hهeû 9փ@H g!GH $"" $@H95G@H $gHc=H $hzf|$@H .虽.X@H $#Ys0 $@zf4փ@H g!GH $"" $@H95G@H $gHc=H $hzf|$@H .虽.X@H $#Ys0 $@zf4փ@H g!GH $"zYihh,U $@o;gϞ f hX $@zf+*EH $@+g hX $@zf+*EH $@+4,6LoI=_ [[Z;=/E qcʥR+E`y| 5$" 8qOHAZ*^agr*ΦU0+IgǨ\r-Nsch*gHZ\X嚱K5Vޯ[ V.WY/Hw)yO+G +kAuZW춥Ӭ"J``TNsʶXH,d< b!Mϕ?(M-@H 7?y=BPJ"}ܧ|s0Eu8 #mer:BSk$yqK/ %NJ8WLAʌ2i`56>Twܴon* P "E-2v_RvЪ_-R_Kll7>|u5ۍ KiZTP@HΙ3sfL2L| ss}8?/[dᝓuSۮ}kä[}xtӇ!.-N GlΣ*W/ ,ac&xåݭ/.Z>Ef?wr[2}yw\^vg򙱬;~_گ6ݕFS:[teՒ鶩aݙZڒ1&v^kzzNjw"6.nuk ͭbjμ^7^0'^1)ᘣX9y M^qe=#KF~v @ p34p7m3tM+c S5I˒_O[uHUM7&un%vLV2M*j GBU{ᷕ'09e02X}yRkikOs)ښͣK?1"J@8Q9pهjąvyݷtL)&`८Lj)b=ۖ:7ǦGkQ<]Ĺ}>%f*_xoɼskZ8fg Ɯ6t$|0ݰSO|޹yy E{uyI0\^أ^9M^CAP}+ZFuO7<aÆΡ xBU nKƝE]l [s߭unߴ7+SbqƖ%i,S>KQN쨴++ida_ivMoc@M :Z@r$Q )_YB[7o|9R]w>sѲpsLPxoN?JTYjhՖRoo 殴/jw*j3@(t NxyO/?om3{g^~yg3-<ۉpd_)mW{1԰K?S3odi6j{BԺy̔Pka}]豈%;]QBzA¦][Q[u8wY@@x/|>W6E=۲6dn<ۼܻ!)Tukrf__ZToJ6f漣#W7]KR33yM-{Ū=2\pYrAxz:wEkiv;WU{U9!׼| &ݽ.۴(27F?oطssb^ָ,޼x?XC@7_Xj>;B)g~ERk*DGw}]-- gVXgso?XeRh>`n ̔K;5C[K$ٴm,2o»-)3l,eRvZLG'ZxljLh@D46 2Kovl)%@&}"=*֟'^λs| o)޵Έ6t{؊6 po2,[\'/IXaݼEOog2A(-7o}ȸ ~:̄_؝/^ǫs 'g<:Iǥ.Қ?: ;zh^G+Z]~va] nq @ru~osʗ[™S^iO=̝=)͋.26ݭ/єg.,ZXI:IϮxm]i+^˵XfUmۿfK"~J^Sf.:qͼGǻ`[ڹ<B-Vu2GvM׵;|`_<& IDAT^LG'~hZMxAfof +@v)(Cc^wxS*|U'v'e@dR8pg̘+]IvO2z̬o5_xTl&Çz`|/hM.jw'VdnsGtgץ3.>M j,A׼f J[M<\qf?O=;a[lZȋiX99:ֺtyIttRO ^ 0JzS5*W~~(4 U&<1SF*D@̟gVtݡ^]TK7vg6>8kb_Y)+ @3nP]/ *L'Hx<M`֋!@"q>4?cC'g&unE96@h -n7Nᇇrp_L@¡ Y E?4nQ-*@8FEfwc 8 @@>3@@ _e$h  @d9 Y@@8@@| 2˗    /Df2@ 2@@E,_Fv  Df  @H@@Ȍs@"| ځ q  "@d/#A;@@"3@@ _e$h  @d9 Y@@8@@| 2˗    /Df2@ 2@@E,_Fv  Df  @H@@Ȍs@"| ځ q  "P톄i eԐq$_4ٷՏ¾RIw$  @N?''xR]2/!ſ^Ԫӎgkk֕&*706@@ CSW#)iǓQGFēz  s弇T YD@ 2+1  P(Df2R@_Ȭǘ" @d9q:GoKծs[EނUz|Uխ1{5Ag)]O@8̎U?km-Ng+Wks ]ejKQ.Q8~T)H@@2$ý6Ρ҄, >uRd'mAaWo5 E-9.Rgy`_GUZvwyYs6~bٷOVN:?x|  PH6(瘗ג~wSVMثS&J-\[G&itZR/n:ME*  F W3TQrW^I }|JGkźv"yu%ks mf])\@5YHB@;2ۙ24y&Ήk-Mf HL9-3a^aӠ:z}V4izOR  @E4rrYtsqlwi/4twk7$xy3 xKvpGf9Z;'KSҥT<9C)bl" &9´'tɝik1w=yG=#'%lj@% N>nk)F@0}Pai $ ^Xκ%NA%7=vYz b/jp٬RD@N"kP6rgfu|=U%Ҷ,- ;@@ #_̨Alq~9f⮫#'Դ[W3՞&y^!垲!d&@L;qV fZ/8z4ضtZoO7Ji) IIl" 6%UɝS[jo/G<Ǽ}֫ɇT֛s'꘷?WȖhj78"ASkəF@Ic~oitmu*;1U-A!ԟu 5yW#bI{魵ZQ?;%v(%@@7|̴{>w~;.Td-mИBRS0Y2,_n$# i{4Yܙ;ue&gpNW?rMcg>R.eJN@8ёJ},_tܩ/ݩ5{0x&>y:f I  Ч@#WӺSؙ^M/Чg)1\A1L XNZk'tYk,-qi]gh$e0l! @zzzR38p`3fHݕ.eەoLud[4b>13i둰ݨsmphKٿmȤA@'x@9W:uQ2~RyB*ܠ?&  @~53K@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  %jXϿg$A@ _csfǢ1  @.rJ     2˅*e" "@dv,j B 'YR\WeimK,{*ݠPb$mR#-L6C gXU"iF )K-;Hu&e菧Y/WSJ?4}(mpW9r]{X;?k6)ܣ'Th'>=Gc| ->lОiR]RKf,=+5SĢjBڧmRGSѡ*TX{Lzj2C斴rLڜ´,.vJ=CIidtӛsfܦ53QAUT}[ᚻ̶i)>n+Te>v6uy 9jODKB:d )t9@(M{Le/93hd/3ܪ>rwzyqL0/1Α^0jWY&sNNN%u>?<M-N ifbpW٩\]:X I9ciEpnѩĤ>>Mu3gF@fS;kW?ZpԁICTiXQ^`t'SXI'ߓF-=KHsj}iLLC4kqJبԹH?8wsqg]2?S!^|"Fm&?&?;줓1pyYm?;\5;9͛ZQEzuYl'NB!f:y6%Rm$Sʼ!5Ü}^vockᕹQXN 4mwʉxUHMGnm- DdmҲږ;-oU6nSnT.w۝pۜ9f ۝A[4[7AFs>{wŔZg$7$uڬG3fcxyLK'FΌ4d^mZg[֙@e;͎NQpf+Qn>&ٞz*UnyQU{Դ_J ?"x#6?hNXof:y,cX5:,O<&JE'zr~Ώ=&%& P1V؜3d=2 *&:1gvHoİ[3WZirBJL^zFf!&0{Vsj\|6TEuer.MFC\iY.9"Ͷa]U!e"fE}3wYry_Nc/ug}f"kb2nPK&w7xu4W-"frKڶN`\l7u=j97sw Gv6 Ar/hm]gnSY'Ƶ}Re͉ۜJ6){}"*uy'm>=1;!f,HXصթ 9C9iQ6MdSi? >ڈs"c eTEn3T֨Q McΑ iӰ=aCXUuVJԽsW8-8U79Nh*'Ed?|$"@1 XicYfNέ,Zzg*}V rر5Tu3Qj 7BoSm2X,K.5׽.W&}A{i85Aӌf:L5e{FUfcRӡKLd- 령I-Ǥz]+Qis熫qem1zl TWh$=cp(Z6Nݍ2A[SYF'зd7)Y'W/6w1מmOkm-osi{bܬ'F{!stYZl{jS{Mq.e8 I۽6Gf wwgݪ wyʜRzq_`~햄bݍS+}E65ܦ!13dhf%v#wdDۘ!哻yQ"ziV/vMUd~hb2I8˽Dp595iR汼+6hbQr>֬]W7N EoIz {O3 ^+;&H{.ү͆?Fh/]v|{~Ƭ-GM1kL#ԋ^rzf?R3ߔluby|:Dl^%gq;8ԏo3Co =M1'6SW~xtv8CF)0VLodM[L3?!ɱκWO_WDtIkCpN^-ߨ sy.`ȼnEs1ELXҌ>%1DŽwL9UcEt"i ՄO'8G?Vyʶ SebSb'YxY?b ?o&`A.r[&sD* @Tt~ք;59+!M a0.u״7XiW'"f{ۼ&{||f+Nk}dtؾYzkN};(vSsZ>W~~=nӥM]F' }.7z2W!et^ ,711 G-G}('VڤoW{\80nUnG4C5jK&c0z 8'I/CkWXꆥCW(DaS-Fӝt*=c%ʰ&hA4_7FgK-||3-ο#эusLsęY?=jfAH++Y/ 7;M~e>/4s4շ8x[z4O7Z35%S+M=iڥpce'F6T>kӪڣu6RWjR{et ֻS%/|VSi.;Mo@ю }{gyPcޝ{'1e&*17۹.{hHMν͊lqvzG5TFBАşvr7E6CNy\(߶fۤ'pSURğ4?8lʏ=دɐZD$=o^;OڴžуʴCٜ?C6Sʏ+gYAb$ƞɳCòo.E {_釶Ce-^L4L 4\}}F4L+l˺&%7)3b>._~e-˙]Xkc;K2mFW!K_nM({pmӐ{t#gtĩz{g5ow|" 'Z ˑ.vuJ/iZ6F7OSoYE΅ O.*MӀ&5ifК;מCЪ#F@N@6#gޘ;zbO_M`ze<լҽ @@P 7>GsǼr0¬ΟTl[ M&  ًJ5&M# @>ddϫi~!ғl@u|QվoKFk#ƍ>Sm|H!@N@Zo2= aI}V^{eYPoMBZcEX%LIԄKL>@@-l%޳ͿOS~㽫b9ԇ^eN/wR]==լ̺ ՜R1ɱw_^uv/4XE@+0'X~;xy9]{u}_K5ιНGsQ">TᆬZUלB@@`@ <bXzԩSGՔ5M-WKu}ٶ 2K@@u53]K{Wl絀/O@(\>[uVMΩ% w9:мKBh(mD@Ip̎{ Y ME@" 2+{  P@Df4X4@\Ȭ! @ `T@(r""` $@dV@ES@@̊| Y ME@" 2+{  P@Df4X4@\Ȭ! @ `T@(r""` $@dV@ES@@JտVI ̎Ec@@\B2@@c 2;5A@r!@d UD@EX8@ȅ@."UZBkҚ m־pR7)-NJM޼n4VL~[lU_&laR^78|ӟĺzޗ bGe* @OI&$gʼHzBjrQj6q^jWmѼrf=*[T/"KMxEJU:]6:Ek-mst-6GBjܭ6L'+ƒJ/umkrV_Y*ǐnV.1bVʵ6fU%N}յĴ!6G!U(Rclw3nk֜"u!#ޥ:^_sX3%|@oASwˤ+uIQd|x|oz݄nR|y)ֆ#]M _ñȬlmr#%D72PM*zw4-cʻT܆-jknB3p&ܧ*&쩨[٧5ΦkTCs묉m׋U\'szjY"MNV6 [~NͭwbӰwbMl6I"3aהۼu0N֘јfmG:0d^b-a@$"\\L;x\&ԣxn4fO߶h Xw%/EIiBwjѭ ~GW4Bz/5|_j#Eu>L-eɍzT<`"՛'=նhM_jEr}umZP?m/ӈ`O=żR-6xĻ i.攪nY#-T#{TgciYg<[MfGIpdƮ̺\m,3&ph4iBux_֘Vmw@R+O@@~of&\JenjJXkP)yB.mhm/w15vq?h Ф(=@Ŵr;dt١^/҃YOkb2FZv[߭Zw\v/JꅷP&Խ0vp|c-->ꄾNEq7)}h]Hw^jjf{K%`V@(Z~3saDeU]Ѡר- \8,4_e]fA7lN |K{ st/\y4Gej-[tsvՐnr  @Gf^w,oڼӭ2oLF`gl@hmw]xTa@$ƞd2X~3RwM4(a5Q[NK t[&Ruۗ.MtyY\gÛG]Lzk-3MǤ"͠>3&yX+V%qsh,  @PeW:uQ-2K`PFfuy.5G@82tCy/Y' P@Df4XԆɾ˸8ڃ @! ϳG@@l eS@@ 2;=E@)ݕfQ  P%ˠ?̙eD@@_J@@ 2@"   /DfL%  @Df @"~a@@ " Ȃ Y0S   YHdA@EȬ_@@,$  "@d/T d hF@@`<~,dX8@ȅY.T)@8"cQ@@ DfPL@@X̎Ec@@\dLY}d_҇z:EvW:mn) M"  P9s BR^+u9Q  (4G?SqX+??ԮVt]C&-@@`$2u:M6Rٹn-OGz25zɑNug@P9*jV7iZH-֦͚ަiZ.S0>Y@@+,\$|}LS.*p_2 vWIXOިJu˿j :  úA5sJ=pʋt ל̐gVI4Bs@@@NyJ˴AyaZzJόK7J+uCzD@@.̖|G NcӪ ]q~kǴE?sv\*5Aj2F# Ed6%SwmnmSQejnяM@  A====pg̘+mʠAiw%03:Rc @(+]W^=uQFb,]M1LRSIA@@NH@ȚY()@8N"p@@ kDfY @@8̎@@ dAweM @k2sf @"~a@@ " Ȃ Y0S   YHdA@EȬ_@@,$  "@d/T d @dY@@~ 2f*A@2 2,  @ 3  eD@@_J@@ J2ȓQ @@ sfi`HF@] ksfGIC\tw2O.wdj]iR @##[/?4co޷:CpoSz[W[e~{nn{Ug1X@@Bie{_{5Zg/uIv$_X;I~PI  vdvec./m|[>{+ӿK>[o`C|kw}##ηtI86w2 P,Fugl({׽r'2/gi~iՊ_72of^1Dϫ6MSڪ?|X8o}eXYA@|Yڞ>;׌k 7VFòx/Dò)S&}M?{zӻfdx(tuxOM 5- ˜=Q;~h,  @^ d52 Ͷw ?egCv?-ә7߻ګuvg诿/~kN- w|koL7;lv|?6Ƈ{i?밟,  @> dj[_u/46l|͒N&>=oq&^pHgɨ3LtC{zKBLyMd"ݱYx>/ijh^)m1L 4;y'}`g/cko.%c=ye_S㯽TO+ho SҰw]ޚ8~zOM\5fo,8+1PV@@ dism5Kca,9} e?G_EGwMs4aY8d̫8>o]8߽<{KPw>-zrP65 o[xSkNCxWypw,pwpO9 .T@@ 7ƕR@@ 2;z3@@r#@dWJE@^ kmnGN@@_9cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! ,~f ڍIENDB`lamson-1.0pre11/doc/lamsonproject.org/input/blog/index.txt0000644000076500000240000000015711203535414023212 0ustar zedshawstaffFrom: Zed A. Shaw Title: Lamson Project Blog Content-Type: text/blog Item-Template: input/blog/template.html lamson-1.0pre11/doc/lamsonproject.org/input/blog/template.html0000644000076500000240000000014511203575635024051 0ustar zedshawstaff

$date : $title

$content

Read more...

lamson-1.0pre11/doc/lamsonproject.org/input/contact.txt0000644000076500000240000000064011204533220022602 0ustar zedshawstaffFrom: Zed Title: Getting Help With Lamson There are "mailing lists":/lists/ available (running Lamson of course) that you can join to get help. Keep in mind that these lists are running Lamson, and since Lamson is still a work in progress they might be faulty. The best way to get help with Lamson is to email me directly. You can send mail to "zedshaw@zedshaw.com":mailto:zedshaw@zedshaw.com to ask for help. lamson-1.0pre11/doc/lamsonproject.org/input/docs/0000755000076500000240000000000011313464573021355 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/docs/bounce_detection.txt0000644000076500000240000004355411243654672025445 0ustar zedshawstaffTitle: Bounce Detection Lamson supports intelligent bounce detection with its "lamson.bounce":http://lamsonproject.org/docs/api/lamson.bounce-module.html module. The bounce handling is based on a probability that, depending on found headers, the message is a bounce. It then gives you a nice clean interface to check who it's from, the originating SMTP server, the error messages, and any human readable messages. h1. How Bounces Actually Work Figuring out how a bounce is actually handled is a bit difficult because the majority of the information available is written by people who know very little of SMTP server operations. When the average programmer thinks of handling a bounce, she usually has one of these concepts in mind: # The message could not be delivered, so the remote SMTP server sent back a bounce message. # The message could not be delivered, so the local SMTP server sent back a bounce message. # The message could not be delivered, so the recipient's email client sent back a bounce message. # The message could not be delivered, so Lamson crafted a bounce message. Obviously logically you can't have the recipient's email client send you a bounce unless the user does something weird (and incredibly annoying). This makes sense because the message was @not delivered@. How can the email client send back a bounce if they don't receive the message. bq. Yes, some clients do support sending bounces, but very few people use this feature. If you do please talk about it as one more edge case to deal with. Next Lamson can't craft the bounce messages for you because Lamson is simply trying to deliver outgoing mail to a smart-host. Lamson does nothing but ask the smart-host (your local SMTP server) to deliver, and then waits for a response. That means Lamson is not sending you any kind of bounce unless you write code to make it do that. That leaves only two options for @who@ is really sending the bounce message: the remote MTA or the local MTA. The truth is it's a little bit of both. h2. Your Local MTA Is A Person How a bounce works in practice involves two SMTP servers: your local smart-host, and the remote server that it tries to connect to for delivery. The message you actually get in lamson is from the @local@ server, and usually an address @at@ that local server. It does not come from the remote server, but inside your bounce will be a message and status indicators from the remote server indicating why it bounced. What happens is your local server attempts to deliver, and the remote server rejects it for some reason. Now your local server is supposed to try again in certain situations, but after a certain number of rejections or failures it crafts a bounce message. That bounce message is then returned to your @lamson@ server based on the @envelope@ header of the message (more on that later). Now, what's inside this message? Well, it's a oddly nested barely standardized pile of random other messages. This is the frustration with bounce handling. You pretty much either have to use a probability mechanism (like Lamson does) or you have to craft your bounce handling for your very specific local server and any other possible servers you talk with. Even then you can have problems dealing with bounces reliably. Inside this description is an important concept to understand: bq. Lamson does NOT process bounces from the remote MTA or the remote user in any way. Lamson process bounces from the @local@ SMTP server. This is important because if you try to use the bounce message as if it comes from the remote user, then you'll accidentally put your @local@ server into a state that prevents you from processing future bounce messages. If you did not get that, reread this whole section again until moving forward. h2. "Hard" vs. "Soft" Another complexity in dealing with bounces is the concept of "Hard" vs. "Soft" bounces. My opinion is that the distinction is fairly retarded since it is almost entirely unreliable and has no meaning to someone trying to handle a bounce. In popular terminology the main difference is this: * Hard bounce means that person does not exist, so I can not sell him any more crap and need to remove him (maybe). * Soft bounce means that person still exists, but my marketing bullshit didn't get through, I should try again 10 or 15 more times until he gets my important message about winning the lottery. You may be laughing, or screaming various pedantic specifics, but the above two distinctions are both how many email services look at bounces, and how most malicious mail users view them. For the mail services, leaking a hard or soft bounce is a security problem because it tells a malicious sender if that address is a valid person, why the sender was blocked, and how they can work around it. This is why many of the error messages you get back are vague and mostly lies. The major email services just don't want to give you any information that you can use to circumvent their anti-spam measures. bq. The services that have the strictest anti-spam measures also have the most nasty disgusting spam on the web pages displaying user's mail. Yahoo is both the worst for erroneously blocking email and for showing the worst most tricky spam all over every square inch of their mail service. How does this relate to your Lamson application? It basically says that you should treat bounces as being basically soft bounces all the time, and then tune your rules heuristically over time as you run into more and more. h2. VERP The final topic to touch on before getting into how Lamson handles bounces is one of VERP. Remember in the description of bounce handling above I said that your @local@ MTA sends the bounce message back to the @envelope from@ not the From in the headers (well, most ones will). Because of this you can have a From in the envelope that is only replied to when there's a bounce, and then put the real From in the headers for normal processing. This is effectively what "Variable Envelope Return Path":http://en.wikipedia.org/wiki/Variable_envelope_return_path does to process bounce messages. Rather than attempt to parse the body of a bounce message, VERP rewrite the From address so that when a bounce is returned, you only have to process the returned address to see what the original was. Lamson could potentially support this, but it has several problems for generic bounce handling which meant that actually parsing bounce bodies was a better option. h2. Using Lamson's Bounce Handling Using Lamson's bounce handling is fairly simple. It involves the following process: # Import a special decorator @bounce_to@ from "lamson.bounce":http://lamsonproject.org/docs/api/lamson.bounce-module.html # Create two (or one) handlers to deal with bounces. # Place the decorator on any @START@ entry points to your handlers that can receive bounces, pointing them at your handlers. # Handle the bounces in your handlers, making sure to return back to the @START@ state for the local MTA (remember, the local MTA is a person for bounce handling). Here's some simple code that does exactly this by just ignoring bounces from the "myinboxisnota.tv":http://myinboxisnota.tv/ example:
from config.settings import BOUNCES
from lamson.routing import route
from lamson.bounce import bounce_to

@route(".+")
def IGNORE_BOUNCE(message):
    bounces = queue.Queue(BOUNCES)
    bounces.push(message)
    return START

@route("start@(host)")
@bounce_to(soft=IGNORE_BOUNCE, hard=IGNORE_BOUNCE)
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING
This example is stripped down from the real code so you can see what's going on. If we walk through this you can see what happens: # First we import the @BOUNCES@ variable from @config.settings@ which is just the queue we want to dump bounces into. # We then create a special handler named @IGNORE_BOUNCE@ that accepts a message to anything in its @route@ and just puts the message in the @BOUNCES@ queue. # This @IGNORE_BOUNCE@ handler then immediately returns @START@ so we go back to the START state. # On the @START@ state you'll see that we have our @bounce_to@ declaration that points for @hard@ and @soft@ bounces at @IGNORE_BOUNCE@. # This decorator wraps your handler in a little check that, if the message is a bounce, your @START@ state won't get called, and instead your @IGNORE_BOUNCE@ state will. That is pretty much all you need to deal with to re-route bounces somewhere else. You can also redirect them to a totally different handler, which is exactly what the "librelist.com":http://librelist.com/ example does. h2. How It Works How does Lamson figure out that something is a bounce? What Lamson does is it assumes bounces will have some or all of these headers:
BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}
The problem traditionally with parsing a bounce message was that, while you need to find all of these headers, there was no real standard as to how the messages in the bounce message are structured. From my "postfix":http://www.postfix.org/ server the bounce message is a sequence of about 6 nested attachments containing other messages, and sometimes the nesting goes three deep. Rather than rely on this structure (which changes all the time) or that these headers are always present (they aren't), Lamson takes a probabilistic approach based on the number of headers and properly formatted values it finds in @all@ nested attachments. The process goes something like this: # Traverse all the possible nested attachments. # Try to find each header in the attachment. If it's found add a point. # If the header is found, use the regex associated with it (above) to try to match the value. ## If the value matches, then keep the regex captures for later. Add another point. # For each header found, and any regex captures that matched the bodies, put them into an internal dict for analyzing the bounce information. # Finally, calculate a probability score that is the total number of BOUNCE_MATCHERS * 2.0 / points. In general, if a message is found that has a 0.3 or higher bounce probability then it is considered a bounce message and you can look at it. The @bounce_to@ decorator has a threshold you can adjust if you want to be more or less strict. The final result of this processing (which is actually very fast) is that any calls to @MailRequest.is_bounce@ will either return True or False, and then after you call is_bounce you can access the @MailRequest.bounce@ attribute to analyze the information. That information is captured and cooked into a "BounceAnalyzer":http://lamsonproject.org/docs/api/lamson.bounce.BounceAnalyzer-class.html object. h2. What It Looks Like To Receive One It's also instructive to see what it looks like when Lamson processes a bounce message. Here's the "librelist.com":http://librelist.com/ server processing a bounce message:
2009-08-21 13:43:47,223 - root - DEBUG - Pulled message with key:
'1250876622.V8e00I219de0M128371' off

2009-08-21 13:43:47,231 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From: u'"SPAMMER"
', to To [u'lamson@librelist.com'].

2009-08-21 13:43:47,251 - routing - DEBUG - Matched u'lamson@librelist.com'
against START.

2009-08-21 13:43:47,332 - routing - DEBUG - Message to
set([u'lamson@librelist.com']) was handled by app.handlers.admin.START

2009-08-21 13:43:57,367 - root - DEBUG - Pulled message with key:
'1250876627.V8e00I219661M719350' off

2009-08-21 13:43:57,381 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From:
u'MAILER-DAEMON@librelist.com (Mail Delivery System)', to To
[u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com'].

2009-08-21 13:43:57,410 - routing - DEBUG - Matched
u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com' against START.

2009-08-21 13:43:57,431 - routing - DEBUG - Message to
set([u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com']) was
handled by app.handlers.admin.START
These log messages show the following interaction: # SPAMMER@hotmail.com tried to spam the lamson@librelist.com mailing list. # They were required to subscribe, so Lamson sent them a confirmation mail. # That message bounced, so Postfix sent back a bounce message from MAILER-DAEMON@librelist.com to Lamson. # This message from MAILER-DAEMON is a bounce, so the librelist code handled it on the START state, NOT the CONFIRMING_SUBSCRIBE state. # Internally, librelist looked up the target user and just zapped them. That shows how the bounce doesn't come from SPAMMER@hotmail.com nor any server at hotmail.com, but instead from MAILER-DAEMON@librelist.com. You could also get messages from a remote MTA, but if they were found to be bounce messages then that remote MTA would be treated like your own MTA. h2. Gettting Bounce Information From BounceAnalyzer The "BounceAnalyzer":http://lamsonproject.org/docs/api/lamson.bounce.BounceAnalyzer-class.html does the work of figuring out additional useful information you can use to determine what to do with the bounce. It looks at the final headers that are scanned in the above process and pulls out important information everyone needs. The list of information you can get is: * primary_status -- The main status number that determines hard vs soft. * secondary_status -- Advice status. * combined_status -- the 2nd and 3rd number combined gives more detail. * remote_mta -- The MTA that you sent mail to and aborted. * reporting_mta -- The MTA that was sending the mail and has to report to you. * diagnostic_codes -- Human readable codes usually with info from the provider. * action -- Usually 'failed', and turns out to be not too useful. * content_parts -- All the attachments found as a hash keyed by the type. * original -- The original message, if it's found. * report -- All report elements, as lamson.encoding.MailBase raw messages. * notification -- Usually the detailed reason you bounced. But, refer to the documentation for more accurate listings. An important feature is that the status codes are parsed and converted into a standard list available in @lamson.bounce@ based on their numeric values. Rather than parse the details given by the remote MTA, you just use the number codes to get a human readable output. The best way to see all that's possible is to take a glance at the Lamson unit test for the BounceAnalyzer:
def test_bounce_analyzer_on_bounce():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
    assert_equal(bm.bounce.primary_status, (5, u'Permanent Failure'))
    assert_equal(bm.bounce.secondary_status, (1, u'Addressing Status'))
    assert_equal(bm.bounce.combined_status, (11, u'Bad destination mailbox address'))

    assert bm.bounce.is_hard()
    assert_equal(bm.bounce.is_hard(), not bm.bounce.is_soft())

    assert_equal(bm.bounce.remote_mta, u'gmail-smtp-in.l.google.com')
    assert_equal(bm.bounce.reporting_mta, u'mail.zedshaw.com')
    assert_equal(bm.bounce.final_recipient,
                 u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')
    assert_equal(bm.bounce.diagnostic_codes[0], u'550-5.1.1')
    assert_equal(bm.bounce.action, 'failed')
    assert 'Content-Description-Parts' in bm.bounce.headers

    assert bm.bounce.error_for_humans()
Here you can see that you can figure out if the bounce is hard vs. soft, get a human description, access status codes of various flavors, get the remote MTA's name, the reporting MTA (your local), who it was originally for (final_recipient), and even access the raw @bounce.headers@ if that's not good enough. h2. Augmenting The Matchers Another advantage of this method of processing the bounces is that if your SMTP server crafts something that hasn't been handled, then you can augment the matchers being used. Simply update the @lamson.bounce.BOUNCE_MATCHERS@ dict with your new ones and make sure to update @lamson.bounce.BOUNCE_MAX@ to be 2 times that. The status codes are also available in the same way. Refer to the source for more information. One tricky part of Lamson's bounce handling is that it does assume a certain structure for the BounceAnalyzer to get at the internal details. This structure is the one used by Postfix, and it should be the same for other servers. However, if you run into a structural difference, report the results back so the handling can be improved. h2. A More Complete Example Finally, the "librelist.com":http://librelist.com/ example code has a much more complete example of using bounces to disable users and shift their state. Rather than describe it in detail here, I'll simply point you at the "source releases":/releases/ so you can see it for yourself. Look in @examples/librelist/app/handlers/bounce.py@ to see how it all works. In fact, studying how this is triggered from the rest of the librelist example is a great way to learn how to use Lamson in an advanced fashion. Study well. h2. Conclusion Lamson bounce handling is very advanced and can deal with a wide range of scenarios. It should work with a wide range of bounce styles and other servers, but feel free to report your own experiences and differences. lamson-1.0pre11/doc/lamsonproject.org/input/docs/confirmations.txt0000644000076500000240000003057611310760160024770 0ustar zedshawstaffTitle: Confirmations You never know who's going to send an email to your Lamson application. It may be a real person, or a spam bot. Currently most of the spam bots aren't very intelligent (probably because they haven't discovered Lamson yet) so a simple easy way to weed out the randomness is to confirm people at important "choke points" in your application. Lamson provides a simple API in "lamson.confirm":http://lamsonproject.org/docs/api/lamson.confirm-module.html to help you send out and verify confirmation emails. You provide a storage class for keeping track of who you're expecting to confirm, a template for your message to send, and it does the rest. It's even advanced enough to keep the original email around so you can resend it after the confirmation. h2. The General Theory The confirmation API assumes a few things about how you're doing your confirmations: # You want them to reply to an email to confirm. # The address they reply to confirm has a "target" to differentiate it from other parts of your application that needs confirmation. # The address they reply to also has a hex formatted randomly generated UUID for security, and your storage can handle that length. # You want to store the message they sent so you can get it back after they confirm. With those fairly reasonable assumptions you only need to then setup the confirmation API in your @config/settings.py@ file and use it in your handlers, preferrably at your @START@ state and anywhere else that you need to validate the user before they do something destructive. h2. Simplest Usage The most basic usage of the "lamson.confirm":http://lamsonproject.org/docs/api/lamson.confirm-module.html API is to put it in your @config/settings.py@ and then access it in your handlers. You configure it like this:
from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage()
CONFIRM = confirm.ConfirmationEngine('run/pending')
This puts a variable in your settings.py that you can access from your handlers to craft and verify the confirmation messages. bq. By default uses a simple in-memory dict to store the confirmations. That's fine for testing and an initial deploy, but you'll probably want to switch to a permanent form of storage for later. Further on in this document you'll see how. Next, you need to use it in your @START@ state, and then in a @CONFIRMING@ state. Here's a simple example:
from config.settings import relay, CONFIRM

@route("start@(host)")
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING

@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('start', message['from'], id_number)

    if original:
        welcome = view.respond(locals(), "mail/welcome.msg", 
                           From='noreply@%(host)s',
                           To=message['from'],
                           Subject="Welcome")
        relay.deliver(welcome)

        return PROTECTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING
Here's how the above code works: # First we import the CONFIRM variable so we can use it. # In our @START@ handler (which is accepts start@(host)) we use the API to send out the confirmation message they should reply-to. Notice how we give a "start" target as the second argument, this is important. # Then we transition to CONFIRMING and wait for them to reply to that message. # The user then replies to the message we sent, so we handle the CONFIRMING state. Notice that we are handling "start-confirm-(id_number)" as the initial message, with "start" being the target (2nd parameter) from our above @CONFIRM.send@ call. # In CONFIRMING we use the @CONFRIM.verify@ method to validate that it's from the right person, to the right target ("start") and that they got the secret (id_number) right. # Finally, if it's right we send them a welcome message, and if it's not we just ignore the message. An alternative to ignoring the failed confirmation from them is to cancel it and go back to the START state for them. The danger with that method though is a spam bot will get into a loop where you are sending them constant confirmation messages in a loop between START and CONFIRMING. It's best to drop it, and maybe provide a "cancel" mechanism. h2. Using Shelf Storage Other than a few other methods, there's only a need to change the storage. The simplest change is to provide a dict-like interface to the "lamson.confirm.ConfirmationEngine":http://lamsonproject.org/docs/api/lamson.confirm.ConfirmationEngine-class.html to store. Easiest available is the Python "shelf":http://docs.python.org/library/shelve.html module which gives a dict interface to various key/value storage backends. To use one, just change your code in @config/settings.py@ to be like this:
import shelve
from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage(db=shelve.open("run/confirmationsdb"))
CONFIRM = confirm.ConfirmationEngine('run/pending', CONFIRM_STORAGE)
All you do is create a "lamson.confirm.ConfirmationStorage":http://lamsonproject.org/docs/api/lamson.confirm.ConfirmationStorage-class.html and give the @db=@ parameter a dict it can use. Everything else will be the same. bq. There might be thread issues with this, and it will definitely fail if you use mulitple processes. See the next section on using a Django Model. h2. Using A Django ORM Model In the "librelist.com":http://librelist.com/ example code you'll find that it stores the confirmations in the Django model in the @webapp/librelist@ directory. This is actually easily setup, so first read "Hooking Into Django":/docs/hooking_into_django.html to learn how to access a Django ORM. After that, you write a simple version of @ConfirmationStorage@ that would look something like this:
from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()
This is from Librelist, so you see we just import the @Confirmation@ model and then wrap it with the @get@, @delete@, @set@, and @clear@ methods that @ConfirmationEngine@ needs to run. For completeness, here's what the Django @Confirmation@ model looks like:
class Confirmation(models.Model):
    from_address = models.EmailField()
    request_date = models.DateTimeField(auto_now_add=True)
    expected_secret = models.CharField(max_length=50)
    pending_message_id = models.CharField(max_length=200)
    list_name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.from_address
Final step is to configure it in your @config/settings.py@ thusly:
from lamson import confirm

...

from app.model.confirmation import DjangoConfirmStorage
CONFIRM = confirm.ConfirmationEngine('run/pending', DjangoConfirmStorage())
That's all there is to it. This is actually a nice setup because you can use the Django Admin to manage it during your first deployments. h2. Other ORM For other ORM systems simply use the same pattern as the Django example above. You just create a similar model, wrap it with your own version of @ConfirmationStorage@ and plug it into the @ConfirmationEngine@ you use. h2. Targets The only other thing to understand is why the API has a "target" parameter. Let's look at the call to CONFIRM.send again:
CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
The "start" string as the second parameter acts as a the target. It says that this user needs to confirm for the "start" target when they do their reply. That's why the @route@ on @CONFIRMING@ is then like this:
@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')
You could also make the above a pattern, for example in the Librelist confirmations we're confirming that the user is joining a certain mailing list:
@route('(list_name)-confirm-(id_number)@(host)')
def CONFIRMING_SUBSCRIBE(message, list_name=None, id_number=None, host=None):
    original = CONFIRM.verify(list_name, message['from'], id_number)
    ...
This way, the user could have multiple simultaneous confirmations going for different lists and they won't step on eachother. Without this differentiator, you'd have to either restrict users to just one confirmation at a time, or you'd end up getting the data all confused. h2. Confirming Off A Web Link If you want people to go to a web link instead of simply replying, then you have to do the following: # Either write your own version, or subclass @ConfirmationEngine@ so that it uses an address they can't reply to. # Make sure you use an ORM that can access your database and store both the confirmation info, and each users's state. # When the user hits the link you give them and does whatever you need, use the web framework's ORM to validate their confirmation. # Once your web framework has validated their confirmation, then change their state *in Lamson's state* using *your web framework ORM* out of CONFIRMING and into the next state. Assuming you're doing this all with Python it should be fairly trivial. h2. Confirm Only By Web Is Bad I would advise against this method though, since it doesn't really confirm that the email address you received worked. One of the purposes of doing a confirmation email exchange is to make sure that this person can both *send* and *receive*. If you have to point them at your web site, consider having this process instead: # Their first interaction with your service sends out an email that sends them to a web page, and transitions them to a @CONFIRMING@ state *but do not send them a confirmation reply address from Lamson.* You'll actually "delay" this until they fill out your web forms. # In your web framework, you have them fill out forms and such, and then send them the *real* confirmation message using Lamson. Since the Confirmation API is Python you could do this directly in any Python web framework. You're basically moving the call to @CONFRIM.send@ from your @START@ handler into the web framework. # Then your web framework will have sent them a real confirmation email, not just a link, so when they reply, continue with the usual Lamson confirmation process described above. Doing it this way ends up being a good balance between too many clicks and replies, but too few to confirm that the end user can actually reply to email you send them. h2. The Pending Queue You should also notice in the above examples that the original message is stored in a "pending queue" and then given back to you later. This is handy for either finishing their original request without further intervention, or inspecting what they original wanted to do. In the original Librelist code I would take their first message, confirm them, and then pull it out of the pending queue to send it on. This turned out to not work because socially people "subscribe" with a garbage first message, but technically it worked great. bq. You may want to periodically go through this queue and purge any messages that aren't found in the ConfirmationsStorage. Probably with a simple Python script and a cronjob. h2. Conclusion The Lamson confirmation API encapsulates a pattern for confirming potential users. Feel free to suggest improvements to the API if you find further patterns that are needed. lamson-1.0pre11/doc/lamsonproject.org/input/docs/deferred_processing_to_queues.txt0000644000076500000240000001760111242707504030223 0ustar zedshawstaffTitle: Deferred Processing To Queues As of the 0.9.2 release there is preliminary support for deferring handling of a mail message to a queue for another process to deal with in a separate handler. This support is rough at this time, but still useful and not too difficult to configure. As the feature gets more use it will improve and hopefully turn into a generic "defer to queue" system in Lamson. What is meant by "defer to queue" is simply that you take messages your state function receives and you dump them into a maildir queue. You then have another separate process read from this queue and do the real work. Potentially you could have many processes deal with this work, and they could even be on multiple computers. h2. A More Concrete Example Imagine that you have a blog posting system and you want to update a big "front page index" that shows recent posts by your users. However, you don't want to generate this index on *every* blog post users make, since that could involve expensive computation and hold up other threads that need to deal with more urgent email. The solution is to do the minimum quick processing you can in your POSTING state function, and then use the "lamson.queue.Queue":http://lamsonproject.org/docs/api/lamson.queue.Queue-class.html to queue up messages meant for "front page indexing". Here's how that code might go:
@route("(post_name)@osb\\.(host)")
def POSTING(message, post_name=None, host=None):
    # do the regular posting to blog thing
    name, address = parseaddr(message['from'])
    post.post(post_name, address, message)
    msg = view.respond('page_ready.msg', locals())
    relay.deliver(msg)

    # drop the message off into the 'posts' queue for later
    index_q = queue.Queue("run/posts")
    index_q.push(message)

    return POSTING
You can see that you just drop it into the queue with @push(message)@ and it's done. What you don't see is how this then gets picked up by another process to actually do somehing with this email. h2. Configuring A config/queue.py In Lamson you are given control over how your software boots, which gives you the ability to configure extra services how you need. By default the @lamson gen@ command just outputs a basic @config/boot.py@ and @config/testing.py@ file so you can get working, and these will work for most development purposes. In this tutorial you get to write a new boot configuration and tell Lamson how to use it. We'll be copying the original boot file over first:
$ cp config/boot.py config/queue.py
Next you want to edit this file so that instead of running an "SMTPReceiver":http://lamsonproject.org/docs/api/lamson.server.SMTPReceiver-class.html it will use a "QueueReceiver":http://lamsonproject.org/docs/api/lamson.server.QueueReceiver-class.html configured to pull out of the @run/posts@ queue you are using in your @POSTING@ handler.
...
# where to listen for incoming messages
settings.receiver = QueueReceiver(settings.queue_config['queue'],
                                  settings.queue_config['sleep'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
# NOTE: this is using a different handlers variable in settings
Router.load(settings.queue_handlers)
Router.RELOAD=True
...
I've removed the code above the ... and below it since it's the same in the two files. Notice that you have a @QueueReceiver@ now, and that you are telling the "Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html that it will use @settings.queue_handlers@ for the list of handlers to load and run. You now add these two lines to your @config/settings.py@:
...
# this is for when you run the config.queue boot
queue_config = {'queue': 'run/posts', 'sleep': 10}

queue_handlers = ['app.handlers.index']
The @queue_config@ variable is read by the @config/queue.py@ file for the @QueueReceiver@ and the @queue_handlers@ is fed to the @Router@ as described above. h2. Writing The Index Handler You now have to write a new handler that is in @app/handlers/index.py@ so that this @config.queue@ boot setup will load it and run it whenever a message hits the @run/queue@. Here's the code:
from lamson import queue
from lamson.routing import route, stateless
import logging


@route("(post_name)@osb\\.(host)")
@stateless
def START(message, post_name=None, host=None):
    logging.debug("Got message from %s", message['from'])
This simple demonstration will just log what messages it receives so you can make sure it is working. There are two points to notice about this handler. First, it is marked @stateless@ because it will run independent of the regular Lamson server, and you don't want its parallel operations to interfere with your normal server's state operations. Second, it uses a @Router.defaults@ named @post_name@ that you would add to your @config.settings.router_defaults@. Once you have all this slightly complicated setup done you are ready to test it. bq. Also note that the examples in the "source releases":/releases/ have code that does a deferred queue similar to this. Go look there for more code to steal. h2. Running Your Queue Receiver Run your logger and lamson server like normal:
$ lamson log
$ lamson start
Next, go look in your logs and make sure it works by running your unit tests:
$ nosetests
................
----------------------------------------------------------------------
Ran 16 tests in 1.346s

OK
Your logs should look normal, but now you should see some files in the @run/posts/new@ directory:
$ ls run/posts/new/
1244080328.M408474P3147Q4.mycomputer.local
That's the results of your @POSTING@ handler putting the messages it receives into your @run/posts@ maildir queue. Finally, you'll want to run your queue receiver:
$ lamson start -boot config.queue -pid run/queue.pid
If you're running the code given above then you should see this in the @logs/lamson.log@ file:
...
DEBUG:root:Sleeping for 10 seconds...
DEBUG:root:Pulled message with key:
'1244080328.M408474P3147Q4.zed-shaws-macbook.local' off
DEBUG:root:Message received from Peer: 'run/posts', From:
'sender-1244080328.22@sender.com', to To
['test.blog.1244080328@osb.test.com'].
DEBUG:root:Got message from sender-1244080328.22@sender.com
DEBUG:root:Message to test.blog.1244080328@osb.test.com was handled by
app.handlers.index.START
Which means your queue receiver is running. You could *in theory* run as many of these as you wanted, as long as their handlers are stateless. When you're done you can stop the whole setup with the following command:
$ lamson stop -ALL run
Stopping processes with the following PID files:
['run/log.pid', 'run/queue.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 3092
Attempting to stop lamson at pid 3157
Attempting to stop lamson at pid 3096
h2. Further Advanced Usage This configuration is debatable whether it is very usable or not, but it works and will improve as the project continues. To give you some ideas of what you can do with it: # Defer activity to other machines or processes. # Receive messages from other mail systems that know maildir. # Deliver messages to other maildir aware systems. # Process messages from a web application, and possibly even generic work. It might also be possible to actually make your state functions transition to the queue handler states by simply having the function return the @module.FUNCTION@ that should be next. Take care with this though as it means your end user's actions are effectively blocked for that event until the next run of the queue receiver. h2. Call For Suggestions Feel free to offer suggestions in improving this setup (or even better code). lamson-1.0pre11/doc/lamsonproject.org/input/docs/deploying_lamson.txt0000644000076500000240000001674611242710567025475 0ustar zedshawstaffTitle: Deploying Lamson Level 1 These instructions will teach you how to setup a completely clean Python 2.6 installation, a virtualenv with lamson, and all the gear needed to run the "oneshotblog.com":http://oneshotblog.com/ software on your machine. You can then "read how to install oneshotblog":/docs/deploying_oneshotblog.html for yourself. Most of these instructions could be easily turned into an automated script, which may happen in the future. For now it is meant to teach you about the typically dirty details involved in setting up a system for the first time. It also tries to avoid various problems with different operating systems, so let me know how it works for you. h2. A Warning Deploying server software is a notoriously nasty process, especially the first 10 or 20 times. Most operating systems do their best to enforce completely arbitrary restrictions on your file layouts and configurations, and every system has different arbitrary restrictions. When you go through these instructions, make sure you stay awake and be ready to delve into why a particular step might not work on your system. There's a good chance you missed something or that there's something just slightly different about your system that makes the step not work. For example, in the parts of this document where I setup "oneshotblog.com":http://oneshotblog.com/ I ran into a problem with "SpamBayes":http://spambayes.sourceforge.net/ dying because it couldn't iterate a bsddb. Problem is this works just fine in the exact same setup on a CentOS machine and was only dying on a MacOSX machine that I later tested. For whatever reason, the exact same setup can't run SpamBayes on OSX, even though it can run with the stock Python 2.5 in OSX. To solve the problem I just had to show you how to disable SpamBayes in the oneshotblog.com code so you could test it. That's just how deployment goes. You get on a machine and start setting things up and then 2/3 of the way through the configuration you find out that something doesn't work. Only choices are to work around the problem (like I did) or try to figure out why your machine is different and fix it. h2. Step 0: Setup A Workplace You'll want a directory to do this in so that you don't screw up your machine. Here's what I did:
$ mkdir deploy
$ cd deploy
$ export DEPLOY=$PWD
That last bit is so you can refer to this deployment directory with $DEPLOY (which I'll be using in the instructions from now on). h2. Step 1: Get Python Many operating systems have old versions of Python, and even though Lamson works with 2.6 or 2.5, you'll probably want to get 2.6 for your deployment. If your OS has 2.6 available then go ahead and install it. If it doesn't have the right Python version, then here's how you can install it from source and use it as your default Python. To do this, just punch in these commands:
$ mkdir source
$ cd source
$ wget http://www.python.org/ftp/python/2.6.2/Python-2.6.2.tgz
$ tar -xvf Python-2.6.2.tgz
$ cd Python-2.6.2
$ ./configure --prefix=$DEPLOY
$ make
$ make install
After this you will have a bunch of new directories in $DEPLOY:
$ ls $DEPLOY
bin     include lib     share   source
Finally, you just have to put this new bin directory into your $PATH:
$ export PATH=$DEPLOY/bin:$PATH
... then you just try it out to make sure that you have the right one:
Python-2.6.2 $ which python
$DEPLOY/deploy/bin/python

Python-2.6.2 $ python 
Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> ^D
That's it, you'll now be able to use this Python when you need to run your Lamson server, and setup a virtualenv (coming next) so that you're walled off from the rest of the system. bq. Operating system fanatics will scoff at putting the python install in this directory, so if you want you can just install it to the default /usr/local on your system and deal with all the various clashes and conflicts you'll have, especially if you are on an MacOSX machine. h2. Step 2: Install VirtualEnv Now we need to create a "virtual environment" to install all your software. To do this we'll need easy_install installed to your $DEPLOY directory:
$ cd $DEPLOY/source
$ wget http://peak.telecommunity.com/dist/ez_setup.py
$ python ez_setup.py 
$ which easy_install
$DEPLOY/bin/easy_install
As you can see, you now have a clean install of easy_install in your fresh $DEPLOY/bin directory for you to use. Now you need to install @virtualenv@:
$ easy_install --prefix $DEPLOY virtualenv
$ which virtualenv
$DEPLOY/bin/virtualenv
bq. Make sure you use @--prefix $DEPLOY@ above or you'll install things into the default system setup even though easy_install is clearly and obviously running from a Python in a totally different location so easy_install should know that. h2. Step 3: Create Your VirtualEnv With that you are ready to setup your virtual environment which will house your Lamson setup and fill it with the gear you need. First up is getting your virtualenv created and activated:
$ cd $DEPLOY
$ virtualenv LAMSON
New python executable in LAMSON/bin/python
Installing setuptools............done.
$ cd LAMSON
$ . bin/activate
That's pretty simple, and it tells you clearly that you are using the LAMSON virtualenv. It prepends that to your currently prompt, so your prompt may look different. After that we can use easy_install to install our packages to this LAMSON virtual env. Keep in mind that these packages will be in $DEPLOY/LAMSON, so they won't infect your regular $DEPLOY setup.
$ cd $DEPLOY/LAMSON
$ easy_install lamson
After that, you have lamson installed and ready to go, and you can install anything you want, but there is one catch: bq. You *MUST* be in the $DEPLOY/LAMSON directory or easy_install barfs complaining that the package is not there. h2. Step 4: Making Sure It Works All of this setup is pointless if you can't get back to it later, so exit your terminal completely and start a new one so you can do this:
$ cd projects/lamson/deploy/
$ export DEPLOY=$PWD
$ export PATH=$DEPLOY/bin:$PATH
$ cd $DEPLOY/LAMSON
$ . bin/activate
(LAMSON) $ which python
$DEPLOY/deploy/LAMSON/bin/python
(LAMSON) $ which easy_install
$DEPLOY/deploy/LAMSON/bin/easy_install
(LAMSON) $ which lamson
$DEPLOY/deploy/LAMSON/bin/lamson
(LAMSON) $ cd $DEPLOY
(LAMSON) $ lamson help
If you can do all that, then you know you've got the setup going, now you just need a little shell script to kick this all into gear automatically:
#!/bin/sh

export DEPLOY=$PWD
export PATH=$DEPLOY/bin:$PATH
cd $1
source bin/activate
cd $DEPLOY
To use this script, you just do this:
$ cd projects/lamson/deploy
$ . activate LAMSON
With that you have a fully ready to go setup that's not using your normal system's Python at all, has Python 2.6 installed, a fully virtualenv, and the start of your lamson setup. h2. Conclusion Your next step is to try and setup "oneshotblog":http://oneshotblog.com/ using the "instructions I've written":/docs/deploying_oneshotblog.html to follow these instructions. This document is very fresh, so send me feedback on your experience with running through it. Make sure you tell me what system you are on and that you ran each command exactly when you do. lamson-1.0pre11/doc/lamsonproject.org/input/docs/deploying_lamson_level_2.txt0000644000076500000240000000006011243027730027055 0ustar zedshawstaffTitle: Deploying Lamson Level 2 Coming soon... lamson-1.0pre11/doc/lamsonproject.org/input/docs/deploying_librelist.txt0000644000076500000240000000005411243027650026151 0ustar zedshawstaffTitle: Deploying Librelist Coming soon... lamson-1.0pre11/doc/lamsonproject.org/input/docs/deploying_oneshotblog.txt0000644000076500000240000002261411242710431026504 0ustar zedshawstaffTitle: Deploying OneShotBlog These instructions follow from "Deploying Lamson Level 1":/docs/deploying_lamson.html and you should follow those first before attempting these. If you run into problems with these instructions, then "email the lamson@librelist.com":mailto:lamson@librelist.com mailing list for help. h2. Step 5: Setting Up The OneShotBlog Let's see if we can setup the OneShotBlog example from the Lamson source the way it is on the "oneshotblog.com":http://oneshotblog.com site. We'll need a few more modules installed with easy_install:
$ cd $DEPLOY/LAMSON
$ easy_install markdown
$ easy_install mock
$ easy_install spambayes
Let's grab the 0.9.3 source from "PyPI":http://pypi.python.org/pypi/lamson/0.9.3 so we can get at the OSB example source:
$ cd $DEPLOY/source
$ wget http://pypi.python.org/packages/source/l/lamson/lamson-0.9.3.tar.gz
$ tar -xzf lamson-0.9.3.tar.gz
$ cd lamson-0.9.3/examples/osb
Now we hit a slight snag. OSB is using "SpamBayes":http://spambayes.sourceforge.net/ to do spam filtering, but you probably will have a broken setup and would need to configure a ton of stuff to get it working. For now we're, just going to cheat, since it looks like SpamBayes has problems with trying to iterate the keys in a bsddb under *some* Python builds. To avoid the problem, we're just going to edit @app/handlers/comment.py@ to remove the line with @spam_filter@:
@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
# DELETE THIS LINE IN app/handlers/comments.py
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, user_id=None, post_name=None, host=None, domain=None):
    comment.attach_headers(message, user_id, post_name, domain) 
    confirmation.send(relay, "comment", message, "mail/comment_confirm.msg", locals())
    return CONFIRMING
The spam filtering does work, but SpamBayes is difficult to get working in such a small test run. You are now sitting in the OSB example code, so you can fire up the logger server and run the unit tests to make sure everything is working:
$ mkdir logs
$ mkdir run
$ mkdir app/data/posts
$ lamson log
$ nosetests
You should get two errors you can ignore for now:
..............
======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_unconfirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_confirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured stdout << ---------------------
run/posts count after dever 1
run/posts count after dever 2

--------------------- >> end captured stdout << ----------------------
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 25 tests in 1.363s
Those are just fine since you don't have PyEnchant installed and aren't using the spam filtering. h2. Step 6: Run OneShotBlog Example Now you're running the logger server and have your unit tests going, and hopefully you can fix anything that you run into by now. All you need now is to run the whole setup and try it out:
$ lamson start
$ lamson start -pid run/queue.pid -boot config.queue
$ lamson start -pid run/forward.pid -boot config.forward
With all this gear running you should be able to look in the @logs/lamson.log@ and @logs/logger.log@ to see what's going on. You'll see the following activity: * Forwarding receiver taking mail that couldn't be delivered and forwarding it to the logger server. * The queue receiver pulling messages off run/posts and either delivering them as comments or updating the index. * The rest of lamson processing mail and doing its job of feeding these two or just sending emails. bq. If you want to see the configuration for these two other servers look in @config/queue.py@ and @config/forward.py@ or better yet, diff them against @config/boot.py@ to see what's really different. You should also check to see that they are really running:
$ ps ax | grep lamson
29438   ??  S 0:05.78 python lamson log
29605   ??  S 0:00.63 python lamson start
29612   ??  S 0:00.19 python lamson start -pid run/queue.pid -boot config.queue
29617   ??  S 0:00.34 python lamson start -pid run/forward.pid -boot config.forward
h2. Step 7: Playing With OneShotBlog Now we get to play with it. Lamson comes with a web server that you can run to do simple testing so start up a second window/terminal and do this:
$ cd projects/lamson/deploy/
$ . activate LAMSON
(LAMSON) $ cd source/lamson-0.9.3/examples/osb/
(LAMSON) $ lamson web -basedir app/data
Starting server on 127.0.0.1:8888 out of directory 'app/data'
Now hit "http://localhost:8888/":http://localhost:8888/ with your browser and see the junk left over from your test runs. Most of those posts won't actually exist, so let's make a fake one for now. You can forget about this web server window for now, and go back to your LAMSON window to do this with mutt: # mutt -F muttrc to get it going with a fake setup. # Send an email to first.blog@oneshotblog.com (m is the key). # You'll get a confirmation back, reply to it. # You'll get a welcome message, but this message isn't in the index yet. You can go look at it directly though. # Send *another* email, this time to my.new.post@oneshotblog.com. # No confirmation this time, just a message saying it was completed. # *Now* go look at the index (might take a few seconds, up to 10). # Click on the post title and go look at it. # Right click on the [send comment] link and copy the email address. # Go back to mutt and send an email to that address, this will post a comment to that post. # Reply to the comment confirmation email, this should be the only one you get. # In about 10 seconds you'll see your comment show up. With that you have fully tested out the OneShotBlog example. All that remains would be a full deployment in a for-real situation, which is what we'll do next. h2. Step 8: Running On Port 25 For Real The only problem with testing out the OneShotBlog with your own email client is that you need to trick your computer into thinking your localhost address is also oneshotblog.com. To do that, open your /etc/hosts file and make whatever changes you need to have localhost be oneshotblog.com also. You'll know you've got it right when you can point your browser at "oneshotblog.com":http://oneshotblog.com/ and see your @lamson web@ window display log messages showing you click around. bq. Remember to undo this or you might be annoyed later. Next, you'll need to stop lamson and restart it to use port 25. This will be a problem if you have another server running on port 25, so make sure you turn that server off for now. bq. I hope you aren't doing this on a live site where that's a problem.
$ lamson stop
$ vim config/settings.py
At this point you'll want to change the receiver_config to look like this in @config/settings.py@:
receiver_config = {'host': 'localhost', 'port': 25}
Then you'll want to restart lamson so that it drops privilege to your user after grabbing that port. Easiest way to find out what your user id (uid) and group id (gid) are is to use Python:
Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getuid()
500
>>> os.getgid()
500
>>> ^D
This shows that I'm uid 500 and gid 500, so now I can start my server:
$ sudo lamson start -uid 500 -gid 500
$ sudo chown -R zedshaw ./
That last command just makes sure that lamson didn't accidentally change the permission of a stray file or queue to @root@. With that you should be running your lamson server as your user rather than as root, but still bound to port 25. Here's how to check:
$ ps aux | grep lamson | grep root
If you don't see anything printed out, then you're safe. If you see something running as root, then you've got some work to do. h2. Conclusion I'll leave it to you to actually get your mail client to talk to this OneShotBlog and working. If you have problems, look at all the files in @logs/@ and also use the @lamson queue@ command to inspect the different queues in the @run/@ directory. At this point, you should know enough about setting up a lamson server and configuring a real application (warts and all). You should also have learned how to get a clean Python installation that you can use no matter what your native OS does to Python, even if it's retarded and renames python to Python for no apparent reason. lamson-1.0pre11/doc/lamsonproject.org/input/docs/faq.txt0000644000076500000240000003365211242712465022673 0ustar zedshawstaffTitle: Frequently Asked Questions If a question is missing you can "email the lamson@librelist.com mailing list":mailto:lamson@librelist.com about it and I'll answer. h2. What is Lamson? Lamson is a pure Python SMTP server designed to create robust and complex mail applications in the style of modern web frameworks such as Django. Unlike traditional SMTP servers like Postfix or Sendmail, Lamson has all the features of a web application stack (ORM, templates, routing, handlers, state machines, Python) without needing to configure alias files, run newaliases, or juggle tons of tiny fragile processes. Lamson also plays well with other web frameworks and Python libraries. h2. Where does the name "Lamson" come from? "Lamson Tubes" is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today. h2. What kind of applications do you envision being built on top of Lamson? Why, spam of course (like I could stop that). As well as spam fighters, greylisters, "campaign management" applications, mail firewalls, mime strippers (for those Exchange file shares), help desk support applications, games, mailing lists ('cause everyone loves writing those), and even SMTP portions of just about any web site. A few examples of applications that are actually written using Lamson (with source included in the "source releases":/releases/ * "OneShotBlog":http://oneshotblog.com/ * "Librelist":http://librelist.com/ * "MyInboxIsNotA.TV":http://myinboxisnota.tv/ With many more to come. h2. How do I install lamson on a Debian or CentOS server? Debian and CentOS are notorious for being dinosaurs. Both distributions of Linux suffer from the false rationale that older software is more stable and secure. The reality is that the stability or security of a piece of software is not a function of its age, and in many cases the newer versions of software will typically fix many stability and performance problems. Despite this fact, these two variants of Linux are notorious for back-porting patches from later versions to older versions rather than just using the newer version. In order to help people who need to run a modern piece of software on their antiquated operating systems, I've written an extensive document on "how to Deploy Lamson":/docs/deploying_lamson.html that tells you how to do it in a way that should work reliably on most Unix platforms, even if they have an older version of Python. h2. Is there a mailing list? Yes, and it's written in Lamson. You can send an email to "lamson@librelist.com":mailto:lamson@librelist.com and you'll be able to join. The code for this mailing list system is also included in the "source releases":/releases/ so you can learn from it. h2. Is there an IRC channel? Yes, and it has relatively low traffic. You can join #librelist on irc.freenode.org. h2. How do I report a BUG/Feature/Question in Lamson? Currently the best place to report a Lamson bug is to the "lamson@librelist.com":mailto:lamson@librelist.com mailing list. If you don't want to subscribe to the list to just report a bug, then you can "contact me":/contact.html directly to report it. h2. How do I work on Lamson? I currently do all the work on Lamson myself. I don't want to discourage contributions, but I've found that when a project is small and just getting started it's best to keep it under the control of one person. If you find bugs, then please report them to the "lamson@librelist.com":mailto:lamson@librelist.com mailing list to "to me directly":/contact.html and I'll fix them up. h2. How do I try out (install) Lamson? Best way to do it is to have "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall and simply do:
$ sudo easy_install lamson
Then you'll get it installed and can play with it. Refer to the "getting started":/docs/getting_started.html documentation for more information. h2. How can I get the code to Lamson? Lamson lives on Launchpad at "https://launchpad.net/lamson":https://launchpad.net/lamson where you can get the code via:
bzr branch lp:lamson
Bazaar may ask you to login, but it should still give you the source. Refer to the "download instructions":/download.html for more information. h2. What features are planned for the 1.0 release? I try to make "1.0" what most other people would consider 80% complete. This lets people get it and work with it, and then I can refine it for the 2.0 release with 100% of features people actually use. With this in mind, I'm planning on the 1.0 release to be based on the sample applications and my own applications and include the smallest set of features. The most important part of the 1.0 release will be good documentation and bug free high quality code. h2. What are Lamson's current features? Lamson is currently running a handful of sites and is almost ready for a 1.0 release. It has these high level features: * Very good bounce detection and analysis. * Spam blocking. * Get an application going quickly with the "lamson gen" command. * Full set of "self-documented commands":/docs/lamson_commands.html for managing and developing a lamson application. * Lots of "documentation":/docs/ with more to come. * Sample applications for you to base your work on. * Handle mail for arbitrary hosts and addresses using simple regex routing. * Process requests including full access to Python's complete MIME email libraries. * Absolutely awesome full conversion to Unicode including complete cleaning of incoming mail. * Routing requests in a standard web application framework style based on addresses. * Processing mail messages (requests) in either simple generic handlers, or in more complex and robust state machines. * Use any Python database storage you want. * Use of Jinja2 or Mako templates, with Jinja2 as the default. * Craft and send plain text or HTML email including attachments, with a great HTML mail generation API that even "knits" in CleverCSS. * Unit test helpers for having a conversation, checking spelling, etc. * Defer processing to Maildir queues for offline handling of bigger tasks. * Extensive logging and development tools for debugging your email applications. * Mutt configurations to fake out mutt so that it talks to your server. * 100% code coverage in the unit tests. h2. Isn't Postfix/Sendmail/Exim Faster? That is a tough question to answer actually. If all you need to do is receive and deliver email then a well established traditional email server like Postfix or Exim (please don't use Sendmail) is the way to go. Hands down these servers are the fastest and the best at this job. However, if you need to actually do something smart with your email, like manage many mailing lists or handle support requests, then these servers are definitely slower. The reason is they require that you configure them to take messages they've already received and hand them to a separate process like a Perl, Python, or Ruby script. This separate process then has to parse the message *again*, do its job without stepping on any other processes that might be running (that means locks), and then send response messages back to the server for even more SMTP parsing. With the triple and sometimes quadruple MIME parsing, the heavy weight processes, the difficult to manage locking, and the additional configuration headaches, there's no way traditional mail servers beat Lamson in speed. Lamson only processes a message once, maybe twice if you defer to a queue. Once the message is parsed you get full access to Python immediately, without spawning a separate process. Even if you defer to a queue, the Lamson dequeue server stays resident and processes the queue without forking. You can even run many dequeue servers on mulitple machines processing a shared Maildir if you need the extra processing. In the end, threads and function calls beats processes and pipes. h2. Why not use Sendmail's Milter? Sendmail has a protocol named "Milter" that lets you write a mail processing server that acts as a sort of "slave" to the sendmail process. This protocol is supported by at least Postfix as well, maybe other servers. Feel free to go try Milter. When you're done trying to figure out the protocol from the dense C code, configure the m4 macros, find a decent milter protocol library that doesn't involve installing sendmail, and debugging the final setup, then you can come back and have it easy with Lamson. h2. Why does Lamson send messages to a relay host? Lamson doesn't have to deliver to a relay host, but it is a smarter more practical use of the technology. Lamson is written in Python and does actually run slower than the established mail servers. In addition, Lamson is hopefully doing something more than just routing email around to people. It is probably processing messages, crafting replies, querying databases, hitting REST interfaces, and all the other things you'd want to do with a modern application. This takes time and resources and are probably more valuable operations than just simple delivery. For this reason, you want to use a dumb workhorse like Postfix to do your actual delivery, and reserve the smart processing that has value for Lamson. h2. What about security?! Shouldn't Lamson be 20 processes? Have you ever asked why other mail servers are a bundle of a billion processes? Why have one server receiving mail, another routing it, and another handing it to users? The answer is back in the 1970's most mail was delivered to Unix users in their home directories or similar files that required special access rights to modify. Also in the 1970's special ports like 25 for SMTP required root access, which in the tiny Internet of the time meant that the server could be "trusted". These two realities of the time meant that to receive and deliver mail at least some part of the system had to run as root. To keep things safe, modern mail systems reduce the amount of time spent as the root user by separating their functionality into different processes. However, if you never have to deliver to a user, and all you ever do is process mail and talk to other servers like RDBMS, then why do you need all this privilege separation? Sites run just fine with systems running as one or two processes without the complexity of some illogical privilege separation getting in their way. To put this into perspective, imagine that you were writing a Django application and you were required to have a separate process for the HTTP layers, the view layer, the model layer, the HTTP responses, and the RDBMS access layers? Each one required a different user, a different configuration file, and you needed another process just to keep them all sane. All of this just so that if someone hacks into your HTTP server as root they supposedly can't cause any damage. Yet, they are on your server as root after all. In practice, you can run Lamson as a separate root process, and then use another "dequeue server" to do the real processing, if you feel you need that security. But, consider delaying that decision until you absolutely need it, because the security benefits aren't worth the development and deployment hassles. h2. How come nobody thought of this before? I don't know why, since it did seem kind of simple. There's at least one other project written in Perl called "qpsmtpd":http://smtpd.develooper.com/ that does something similar, and there may be more. If you know others feel free to "contact me":/contact.html and let me know. h2. Isn't [Insert Random Java Mail Server] actually the first mail "framework"? I get this quite frequently when I make the claim that Lamson is the first email framework, and it may be true that there was a framework out there before Lamson. The internet is a big place, so anything is possible. However, I looked really hard and I couldn't find a single *modern* mail framework. All that existed were servers I could use to build a framework. You see, around 2003 or 2004 the concept of "framework" changed. Before then all you needed was a server with an extension API named so that it rhymed with "Servlet". As long as your server provided a way to drop a class into the processing queue and let a programmer handle the request you could call that a framework. The usual end result for these *servers* is that you could use them to build a framework if you wanted, but what you'd get is affectionately called a "frankenstack". You'd grab an ORM from here, a template system from there, maybe a workflow engine, write a Maven or Ant script to manage it, and wire it all together with some lame secret sauce code you think gives you a competitive edge. Then along came the modern frameworks like Django and Rails that included everything you needed in a bundle that you could use right away. They had ORM, templating, routing, higher level request processing, email support, REST support, and anything else you might need for the 80% of your application you don't care about. Some people prefer less of these defaults, some people more, but nearly everyone who has to get a project done prefers more than just an extension API so they can build their own framework. Today if you try to claim "Apache James":http://james.apache.org/ is a framework you'd be wrong. I could *build* a framework with it, but I could just as easily build that same framework with Python, Ruby, sendmail and even postfix. James and friends are just servers, not frameworks. In fact, my experience with James and similar Java mail servers is they are much harder to use than aliases+pipes in Postfix. I now advocate that if your framework doesn't at least support data, views, and high level logic as first class entities then it's just a server. You don't have to use ORM, any particular templating, or Finite State Machines like Lamson does. You don't even have to settle on only one way to do data, views, and logic. You *must* at least support data, views, and logic out of the box so your user doesn't have to go shopping at "APIs-R-Us" just to use your gear. lamson-1.0pre11/doc/lamsonproject.org/input/docs/filtering_spam.txt0000644000076500000240000001343011242713153025112 0ustar zedshawstaffTitle: Filtering Spam With Lamson Lamson supports initial use of the "SpamBayes":http://spambayes.sourceforge.net/ spam filter library for filtering spam. What Lamson provides is a set of easy to use decorators that you attach to your state functions which indicate that you want spam filtered. It also uses the default SpamBayes configuration files and database formats as you configure, so if you have an existing SpamBayes setup you should be able to use it right away. h2. Using lamson.spam Lamson gives you a simple decorator to place on any state functions that should block spam. Typically you do *not* want spam filtering on your entire application, since that would prevent legitimate registrations and put too much burden on your system. It's better to put spam filtering on the "insider" parts, and to have confirmation emails on "outsider" pieces. Instead, what you want is to indicate that your "choke points" are filtering spam using "lamson.spam.spam_filter":http://lamsonproject.org/docs/api/lamson.spam.spam_filter-class.html so that when a spam is received they are put into a "spam black hole". Here's an trivial example where the user is in the POSTING state, and you want everything to work like normal, but if they spam then you flip them into a SPAMMING state.
@route(".+")
def SPAMMING(message):
    # the spam black hole
    pass

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter("run/spamdb", "run/.hammierc", "run/spam", next_state=SPAMMING)
def POSTING(message, **kw):
    print "Ham message received."
    ... 
The line to look at is obviously the @spam_filter@ line, which tells Lamson that you will: # Use the SpamBayes training database @run/spamdb@ for the detection. # Use the SpamBayes @run/.hammierc@ file for your config (optional and ignored if it is not there). # Use @run/spam@ as the dumping ground for anything classified as spam. # The next_state to transition to if they send a spam message. *This is optional, but very helpful.* With this, the @spam_filter@ then wraps your state function, and every message is fed to SpamBayes. If SpamBayes says it's spam then Lamson will dump it into your @run/spam@ and transition to SPAMMING *without running your POSTING state*. Once you are in this new @SPAMMING@ state (or any state you like) you can do whatever you want. You can leave them there, or you can have an external tool that let's you un-block someone. Pretty much any spam handling scheme you want is available. Since your spam is placed into a queue you can inspect it later and check for any accidentally miscategorized mail, then use the SpamBayes tools to retrain for the misdetection. bq. Lamson only classifies mail that is marked as actual spam by looking at the 'X-Spambayes-Classification' header and seeing if it starts with 'spam'. If it is 'unsure' or 'ham' it will let it through. h2. Effectiveness "I've":http://zedshaw.com/ been running a variant of this since the middle of May 2009 and it works great. The code I run is a custom version that fits the weirdness of my email setup but the principles are the same. I'm currently using the above spam filtering, some gray listing, and a few other tricks to block most of my incoming spam. With all the spam block measures I've managed to cut down my spam to about 2-3 a day out of about 100-200 I receive. The majority of the "spam" that gets through is actually email that's classified as "unsure" which I then use to retrain SpamBayes to make it stronger. However, that's my personal server, so in the case of a Lamson application you'll want to be careful that your spam blocking activities don't prevent too much legitimate use. h2. Changing What "Spam" Means You can also change how spam is determined by sub-classing "lamson.spam.spam_filter":http://lamsonproject.org/docs/api/lamson.spam.spam_filter-class.html and doing your own implementation of the @spam@ method. h2. Using SpamBayes An important point about SpamBayes is that it comes with all the command line tools you need to configure and train your database using a corpus of spam you might have. All Lamson needs to do is read this database to determine if it is spam or not. With mutt, I save the message to "=spam", which places the spam in Mail/spam along with all of the others. Then I run this command:
sb_mboxtrain.py -s ~/Mail/spam -d run/spamdb
This goes through the spam mailbox, and any emails that SpamBayes has *not* already classified get used for training. SpamBayes comes with other commands you can "read about":http://spambayes.sourceforge.net/docs.html on their site (if you can find it). h2. Autotraining Lamson doesn't support "autotraining" directly, since it's not clear in each situation what is obviously spam. In my personal setup I know that any email not for registered users is obviously spam, so I can autotrain those. If you want to implement autotraining for a part of your application, then look at the API for "lamson.spam.Filter":http://lamsonproject.org/docs/api/lamson.spam.Filter-class.html and simply use it in the right state function. h2. Configuration Finally, the above sample code is not the best way to configure the spam filter. It's better to put the configuration in @config/settings.py@ and simply reference it from there. In your @config/settings.py@ put this:
SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}
Then change your handler code to be this:
from config.settings import SPAM

@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, ...):
   # this is the better way to do your config
With that you can then change up the configuration as needed in your deployments without having to change your code. lamson-1.0pre11/doc/lamsonproject.org/input/docs/getting_started.txt0000644000076500000240000010461711242713461025310 0ustar zedshawstaffFrom: Zed Title: Getting Started With Lamson Lamson is designed to work like modern web application frameworks like Django, TurboGears, ASP.NET, Ruby on Rails, and whatever PHP is using these days. At every design decision Lamson tries to emulate terminology and features found in these frameworks. This Getting Started document will help you get through that terminology, get you started running your first lamson application, and walk you through the code you should read. In total it should take you about 30 minutes to an hour to complete. If you just want to try Lamson, at least go through the 30 *second* introduction given first. h2. The 30 Second Introduction If you have Python and "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall already, then try this out:
$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc
You now have a working base Lamson setup ready for you to work on with the following installed: * Lamson and all dependencies (Jinja2, nosetests) * Code for your project in mymailserver. Look in app/handlers and config/settings.py. * Two initial tests that verify your server is not an open relay and forwards mail in tests/handlers/open_relay_tests.py. * A "logger" server running on port 8825 that dumps all of its mail into the run/queue maildir. * A config script for mutt (muttrc) that you can use to inspect the run/queue *and* also send mail using Lamson's *send* command. When you're in mutt during the above test run, try sending an email. The included muttrc is configured to use the run/queue as the mail queue, and to use the @lamson sendmail@ command to deliver the mail. This tricks mutt into interacting directly with your running Lamson server, so you can test the thing with a real mail client and see how it will work without having to actually deploy the server. Finally, if you wanted to stop all of above you would do:
$ lamson stop -ALL run
Which tells Lamson to stop all processes that have a .pid file in the @run@ directory. h2. Important Terminology If you are an old SMTP guru and/or you've never written a web application with a modern web framework, then some of the terminology used in Lamson may seem confusing. Other terms may just confuse you or scare you because they sound complicated. I tried my best to make the concepts used in Lamson understandable and the code that implements them easy to read. In fact, you could probably read the code to Lamson in an evening and understand how everything works. Experience has taught me that nobody reads the code, even if it is small. Therefore, here are the most important concepts you should know to get a grasp of Lamson and how it works. * MVC(Model View Controller) -- Model View Controller is a design methodology used in web application frameworks where the data (model), presentation (view), and logic (controller) layers of the application are strictly separated. * FSM(Finite State Machine) -- Lamson uses the concept of a Finite State Machine to control how handlers execute. Each time it runs it will perform an action based on what it is send *and* what it was doing last. FSM in computer science class are overly complex, but in Lamson they are as easy to use as a @return@ statement. * Template -- Lamson generates the bodies of its messages using Templates, which are text files that have parts that get replaced with variables you pass in. Templates are converted to their final form with a process called *rendering*. * Relay -- The *relay* for a Lamson server is where Lamson delivers its messages. Usually the Relay is a smart tougher server that's not as smart, but very good at delivering mail. Lamson can also be run as a Relay for testing purposes. * Receiver -- Lamson typically runs as the Receiver of email. If you are familiar with a web application setup, then Lamson is the inverse. Instead of Lamson runing "behind" an Apache or Nginx server, Lamson runs "in front" of an SMTP server like Postfix. It listens on port 25, handles the mail it should, and forwards the rest to the Relay. This makes Lamson much more of a Proxy or filter server. * Queue -- Lamson can also do all of its processing off a queue. In this setup you would have your normal mail server dump all mail to a maildir queue, and then tell Lamson to process messages out of there. This can be combined with the usual Receiver+Relay configuration for processing messages that might take a long time. * Maildir -- A standard created for the qmail project with stores mail in a directory such that you can access the mail atomically and store it on a shared disk without conflicts or locking. h2. Managing Your Server Your Lamson application is now running inside the Lamson Python server. This is a very simple server based on Python's "smtpd":http://docs.python.org/library/smtpd.html and "asyncore":http://docs.python.org/library/asyncore.html libraries. bq. If you want to know more about how it operates, take a look at the @lamson/server.py@ file in the source distribution. You'll need to use a few Lamson commands to manage the server. You already experienced them in the 30 second introduction, and you can review "them all":/docs/lamson_commands.html or see them by using the @lamson help@ command. Right now you have Lamson running on port 8823 and a "Lamson logger" running on 8825. This means that your lamson server (port 8823) will forward its messages to the logger (port 8825) thinking it's your real relay server. The truth is the logger just logs its messages to logs/logger.log and dumps it into run/queue so you can inspect the results. Before we learn how to manage them and what they do, open up the @config/settings.py@ file and take a look:
from app.model import table
import logging

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

database_config = {
    "metadata" : table.metadata,
    "url" : 'sqlite:///app/data/main.db',
    "log_level" : logging.DEBUG
}

handlers = ['app.handlers.sample']

router_defaults = {'host': 'test\\.com'}

template_config = {'dir': 'app', 'module': 'templates'}
Your file probably has some comments telling you what these do, but it's important to understand how they work. First, this file is just plain old Python variables. It is loaded by one of two other files in your config directory: @config/boot.py@ or @config/testing.py@. The @config/boot.py@ file is started whenever you use the @lamson start@ command and its job is to read the @config/settings.py@ and start all the services you need, then assign them as variables back to @config.settings@ so your handlers can get at them. The @config/testing.py@ is almost the same, except it configures @config.settings@ so that your unit tests can run without any problems. Typically this means setting the spell checker and *not* starting the real server. bq. Lamson can load any boot script you like, see "Deferred Processing To Queues":/docs/deferred_processing_to_queues.html for an example of using this to make a queue processor. The important thing to understand about this setup (where a boot file reads settings.py and then configures @config.settings@) that it makes it easy for you to change Lamson's operations or start additional services you need and configure them. For the most part you won't need to touch @boot.py@ or @testing.py@ until you need to add some new service, change the template library you want to use, setup a different database ORM, etc. Until then just ignore it. h2. settings.py Variables The @receiver_config@ variable is used by the _lamson start_ command to figure out where to listen for incoming SMTP connections. In a real installation this would be port *25* on your external IP address. It's where the internet talks to your server. The @relay_config@ setting is used by Lamson to figure out where to forward message replies (responses) for real delivery. Normally this would be a "smart host" running a more established server like "Postfix":http://www.postfix.org/ or "Exim":http://www.exim.org/ to do the grunt work of delivering to the final recipients. The @handlers@ variable lists the modules (not files) of the handlers you want to load. Simply put them here and they'll be loaded, even the "lamson.handlers":http://lamsonproject.org/docs/api/lamson.handlers-module.html modules will work here too. The @router_defaults@ are for the "lamson.routing.Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html class and configure the default routing regular expressions you plan on using. Typically you'll at least configure the @host@ regular expression since that is used in every route and shouldn't change too often. Finally, @template_config@ contains the configuration values for the templating system you'll be using. Lamson supports either Mako or Jinja2, but defaults to Jinja2. h2. Looking At config/boot.py Programmers need to know how everything works before they trust it, so let's look at the _config/boot.py_ file and see how these variables are used:
from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson.utils import configure_database
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

bq. Don't be afraid that you see this much Python, you normally wouldn't touch this file unless it were to add your own services or to make a new version for a different configuration. For the most part, you can just edit the @config/settings.py@ and go. First you'll see that @config/boot.py@ sets up logging using the @config/logging.conf@ file, which you can change to reconfigure how you want logs to be created. Then it starts assigning variables to the config.settings module that it has imported at the top. This is important because after @config.boot@ runs your lamson code and handlers will have access to all these services. You can get directly to the relay, receiver, database and anything else you need by simply doing:
from config import settings
After that @config.boot@ sets up the @settings.relay@, @settings.receiver@, and @settings.database@. These three are used heavily in your own Lamson code, so knowing how to change them if you need to helps you later. After this we configure the @lamson.routing.Router@ to have your defaults, load up your handlers, and turn on RELOAD. Setting @Router.RELOAD=True@ tell the Router to reload all the handlers for each request. Very handy when you are doing development since you don't need to reload the server so often. bq. If you deploy to production, then you'll want to set this to False since it's a performance hit. Finally, the @config.boot@ does the job os loading the template system you'll use, in this case Jinja2. Jinja2 and Mako use the same API so you can configure Mako here as well, as long as the object assigned to view.LOADER has the same API it will work. h1. Developing With Lamson Now that you've received a thorough introduction to how to manage Lamson, and how it is configured, you can get into actually writing some code for it. Before you begin, you should know that writing an application for a mail server can be a pain. The clients and servers that handle SMTP make a large number of assumptions based on how the world was back in 1975. Everything is on defined ports with defined command line parameters and the concept of someone pointing their mail client at a different server arbitrarily just doesn't exist. The world of email is not like the web where you just take any old "client" and point it at any old server and start messing with it. Lucky for you, Lamson has solved most of these problems and provides you with a bunch of handy development tools and tricks so you can work with your Lamson server without having to kill yourself in configuration hell. h2. Using Mutt You probably don't have another SMTP server running, and even if you did, it'd be a pain to configure it for development purposes. You'd have to setup aliases, new mail boxes, restart it all the time, and other annoyances. For development, what we want is our own little private SMTP relay, and since Lamson can also deliver mail, that is what we get with the command:
$ lamson log
This tells Lamson to run as a "logging server", which doesn't actually deliver any mail. With this one command you have a server running on 8825 that takes every mail it receives and saves it to the @run/queue@ Maildir and also logs it to @logs/logger.log@. It also logs the full protocol chat to @logs/lamson.err@ so you can inspect it. bq. Lamson uses Maildir by default since it is the most reliable and fastest mail queue format available. It could also store mail messages to any queue supported by Python's "mailbox":http://docs.python.org/library/mailbox.html library. If you were adventurous you could also use a RDBMS, but that's just silly. You also have the file @muttrc@ which is configured to trick mutt into talking to *your* running Lamson server, and then read mail out of the @run/queue@ maildir that is filled in by the @lamson log@ server. Let's take a look:
set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
Notice that it's configured sendmail to be "sendmail -port 8823 -host 127.0.0.1" which is a special @lamson sendmail@ command that knows how to talk to lamson and read the arguments and input that mutt gives to deliver a mail. bq. Why does Lamson need its own sendmail? Because you actually have to configure most mail server's configuration files to change their ports before their *sendmail command* will use a different port. Yes, the average sendmail command line tool assumes that it is always talking to one and only one server on one and only one port for ever and all eternity. Without @lamson sendmail@ you wouldn't be able to send to an arbitrary server. With this setup (@lamson start@ ; @lamson log@ ; @mutt -F muttrc@) you can now use your mutt client as a test tool for working with your application. h2. Stopping Lamson The PID(Process ID) files are stored in the @run@ directory. Here's a sample session where I stop all the running servers:
$ ls -l run/*.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/log.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/smtp.pid

$ lamson stop -ALL run
Stopping processes with the following PID files: ['run/log.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 1693
Attempting to stop lamson at pid 1689
You can also pass other options to the stop command to just stop one server. Use _lamson help -for stop_ to see all the options. h2. Starting Lamson Again Hopefully you've been paying attention and have figured out how to restart lamson and the logging server. Just in case, here it is again:
$ lamson start
$ lamson log
You should also look in the logs/lamson.log file to see that it actually started. The other files in the logs directory contain messages dumped to various output methods (like Python's stdout and stderr). Periodically, if the information you want is not in logs/lamson.log then it is probably in the other files. bq. You can change your logging configuration by editing the logging line your config/settings.py file. h2. Other Useful Commands You should read the "available commands":/docs/lamson_commands.html documentation to get an overview, and you can also use _lamson help_ to see them at any time. h2. send The first useful command is _lamson send_, which lets you send mail to SMTP servers (not just Lamson) and watch the full SMTP protocol chatter. Here's a sample:
$ lamson send -port 25 -host zedshaw.com -debug 1 \
    -sender tester@test.com -to zedshaw@zedshaw.com \
    -subject "Hi there" -body "Test body."
send: 'ehlo zedshawscomputer.local\r\n'
reply: '502 Error: command "EHLO" not implemented\r\n'
reply: retcode (502); Msg: Error: command "EHLO" not implemented
send: 'helo zedshawcomputer.local\r\n'
reply: '250 localhost.localdomain\r\n'
reply: retcode (250); Msg: localhost.localdomain
send: 'mail FROM:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'rcpt TO:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'data\r\n'
reply: '354 End data with .\r\n'
reply: retcode (354); Msg: End data with .
data: (354, 'End data with .')
send: 'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nSubject: Hi there\r\nFrom: tester@test.com\r\nTo: zedshaw@zedshaw.com\r\n\r\n.\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
data: (250, 'Ok')
send: 'quit\r\n'
reply: '221 Bye\r\n'
reply: retcode (221); Msg: Bye
Using this helps you debug your Lamson server by showing you the exact protocol sent between you and the server. It is also a useful SMTP server debug command by itself. bq. When you use the supplied muttrc you'll be configured to use Lamson's sendmail (not *send) command as your delivery command. This lets you use mutt as a complete development tool with minimal configuration. h2. queue The _lamson queue_ command lets you investigate and manipulate the run/queue (or any maildir). You can pop a message off, get a message by its key, remove a message by its key, count the messages,clear the queue, list keys in the queue. It gives you a lower level view of the queue than mutt would, and lets you manipulate it behind the scenes. h2. restart Lamson does reload the code of your project when it receives a new request (probably too frequently), but if you change the @config/settings.py@ file then you need to restart. Easiest way to do that is with the restart command. h2. Walking Through The Code You should actually know quite a lot about how to run and mess with Lamson, so you'll want to start writing code. Before you do, go check out the "API Documentation":/docs/api/ and take a look around. This document will guide you through where everything is and how to write your first handler, but when you start going out on your own you'll need a good set of reference material. At the top level of your newly minted project you have these directories:
app -- Where the application code (handlers, templates, models) lives.
config -- You already saw everything in here.
logs -- Log files get put here.
run -- Stuff that would go in a /var/run like PID files and queues.
tests -- Unit tests for handlers, templates, and models.
Lamson expects all of these directories to be right there, so don't get fancy and think you can move them around. The first place to look is in the app directory, which has this:
app/__init__.py
app/data -- Data you want to keep around goes here.
app/handlers -- Lamson handlers go here.
app/model -- Any type of backend ORM models or other non-handler code.
app/templates -- Email templates.
You don't technically *have* to store your data in app/data. You are free to put it anywhere you want, it's just convenient for most situations to have it there. Your @app/model@ directory could have anything in it from simple modules for working various Maildir queues, to full blown SQLAlchemy configurations for your database. The only restriction is that you load them in the modules yourself (no magic here). The @app/templates@ directory can have any structure you want, and as you saw from the @config.boot@ discussion it is just configured into the Jinja2 configuration as the default. If you have a lot of templates it might help to have them match your @app/handlers@ layout in some logical way. That only leaves your @app/handlers@ directory:
app/handlers/__init__.py
app/handlers/sample.py
This is where the world gets started. If you look at your @config.settings@ you'll see this line:
handlers = ['app.handlers.sample']
Yep, that's telling the "lamson.routing.Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html to load your @app.handlers.sample@ module to kick it into gear. It really is as simple as just putting the file in that directory (in in sub-modules there) and then adding them to the handlers list. You can also add handlers from modules outside of your @app.handlers@:
handlers = ['app.handlers.sample', 'lamson.handlers.log']
This installs the handler ("lamson.handlers.log":http://lamsonproject.org/docs/api/lamson.handlers.log-module.html) that lamson uses to log every email it receives. h2. Writing Your Handler This document is for getting started quickly, so going into the depths of the cool stuff you can do with Lamson handlers is outside the scope, but if you open the _app/handlers/sample.py_ file and take a look you'll how a handler is structured. bq. Since Lamson is changing so much the contents of the file aren't included in this document. You'll have to open it and take a look. At the top of the file you should see your typical import statements:
import logging
from lamson.routing import route, route_like, stateless
from config.settings import relay, database
from lamson import view
Notice that we include elements from the @lamson.routing@ that are decorators we use to configure a route. Then you'll see that we're getting that @settings.relay@ and @settings.database@ we configured in the previous sections. Finally we bring in the @lamson.view@ module directory to make rendering templates into email messages a lot easier. Now take a look at the rest of the file and you'll how a handler is structured: # Each state is a separate function in CAPS. It doesn't have to be, it just looks better. # Above each state function is a "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html, "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html, or "stateless":http://lamsonproject.org/docs/api/lamson.routing-module.html#stateless decorator to configure how @lamson.routing.Router@ uses it. # The "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html decorator takes a pattern and then regex keyword arguments to fill it in. The words in the pattern string are replaced in the final more complex routing regex by the keyword arguments after. However, *if you want to use regex directly you can*, "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html just needs a string that eventually becomes a regex. # A state function changes state by returning the next function to call. You want to go to the RUNNING state, just @return RUNNING@. # If any state function throws an error it will go into the @ERROR@ state, so if you make a state handler named ERROR it will get called on the next event and can recover. # If you want to run a state on this event rather than wait to have it run on the next, then simple call it and return what it returns. So to have RUNNING go now, just do @return RUNNING(message, ...)@ and it will work. # If a state has the same regex as another state, just use "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html to say that. # If you have a "stateless":http://lamsonproject.org/docs/api/lamson.routing-module.html#stateless decorator after a "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html or "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html, then that handler will run for *all* addresses that match, not just if this handler is in that state. That is pretty much the entire complexity of how you write a handler. You setup routes, and return the next step in your conversation as the next function to run. The @lamson.routing.Router@ then takes each message it receives and runs it through a processing loop handing it to your states and handlers. h2. How States Are Run The best way to see how states are processed is to look at the "Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html code that does it:
    def deliver(self, message):
        if self.RELOAD: self.reload()

        called_count = 0

        for functions, matchkw in self.match(message['to']):
            to_call = []
            in_state_found = False

            for func in functions:
                if lamson_setting(func, 'stateless'):
                    to_call.append(func)
                elif not in_state_found and self.in_state(func, message):
                    to_call.append(func)
                    in_state_found = True

            called_count += len(to_call)

            for func in to_call:
                if lamson_setting(func, 'nolocking'):
                    self.call_safely(func, message,  matchkw)
                else:
                    with self.call_lock:
                        self.call_safely(func, message, matchkw)

        if called_count == 0:
            if self.UNDELIVERABLE_QUEUE:
                LOG.debug("Message to %r from %r undeliverable, putting in undeliverable queue.",
                          message['to'], message['from'])
                self.UNDELIVERABLE_QUEUE.push(message)
            else:
                LOG.debug("Message to %r from %r didn't match any handlers.",
                          message['to'], message['from'])
What this does is take all the handlers you've loaded, and then finds which handlers have a state function that matches the current message. It then goes through each potential match, and determines which of all the matching state functions is "in that state". This means that, even though you have six state functions that answer to "(list_name)-(action)@(host)" only the one that matches the users current state (say PENDING) will be called next. As it goes through these functions it also loads up any that are marked "stateless" so they can be called as well. Finally, it just calls them in order. If the message results in no methods to call, then it will take the message and tell you this, or put it into an @UNDELIVERABLE_QUEUE@ for you to review it later. bq. Slight design criticism: Currently the order of these calls is fairly deterministic, but you can't rely on it. It's also not clear if *all* matching states should run, or just the first. It currently only runs the first match, but it might be better to run each match from each handler. Suggestions welcome on this. h2. Debugging Routes In the old way of doing routing you would edit a large table of "routes" in your @config/settings.py@ file and then that told Lamson how to run. The problem with this is it was too hard to maintain and too hard to indicate that different states needed a different route. The new setup is great because all your routing for each handler module is right there, and it's easy to see what will cause a particular state function to go off. What sucks about the new setup is that you can't find out what all the routes are doing *globally* in one place. That's where @lamson routes@ comes in. Simply run that command and you'll get a debug dump of all the full routing regex and the functions and modules they belong to:
Routing ORDER:  ['^(?P<address>.+)@(?P<host>test\\.com)$']
Routing TABLE: 
---
'^(?P<address>.+)@(?P<host>test\\.com)$':  app.handlers.sample.START  app.handlers.sample.NEW_USER
   app.handlers.sample.END  app.handlers.sample.FORWARD  
---
This is telling you which regex is matched first, then what those regex are mapped to. This is very handy as you can copy-paste that regex right into a python shell and then play with it to see if it would match what you want. You can also pass in an email address to the @-test@ option and it will tell you what routes would match and which functions that will call:
osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 
If you're working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it'll take an email address and tell you all the routes that match it. h2. THREADING! Lamson takes a lighter approach to how it runs. It assumes that most of the time you want lamson to keep itself sane with minimal locking, and that you want each of your state functions to run in a thread lock that prevents others from stepping on your operations. In 95% of the cases, this is what you want. To accomplish this, Lamson's router will acquire an internal lock for operations that change its state, and a separate lock before it calls each state function. Since multiple state functions run inside each thread, but one thread handles each message, you'll get multiple processing, but each state won't step on other states in the system. However, it's those 5% of the times that will kill your application, and if you know what you're doing, you should be able to turn this off. In order to tell the Router *not* to lock your state function, simply decorate it with "nolocking":http://lamsonproject.org/docs/api/lamson.routing-module.html#nolocking and Lamson will skip the locking and just run your state raw. This means that other threads will run potentially stepping on your execution, so you *must* do your own locking. Now, don't think that slapping a "nolocking":http://lamsonproject.org/docs/api/lamson.routing-module.html#nolocking on your state functions is some magic cure for performance issues. You only ever want to do this if you *really* know your stuff, and you know how to make that operation faster with better controlled locking. The reality is, if you have an operation that takes so long it blocks everything else, then you are doing it wrong by trying to do it all in your state function. You should change your design so that this handler drops the message into a "lamson.queue.Queue":http://lamsonproject.org/docs/api/lamson.queue.Queue-class.html and that *another* Lamson server reads messages out of that to do the long running processing. Using queues and separate Lamson servers you can solve most of your processing issues without a lot of thread juggling and process locking. In fact, since Lamson uses maildir queues by default you can even spread these processors out to multiple machines reading off a shared disk and everything will be just fine. But, since programmers will always want to just try turning off the locking, Lamson supports the @nolocking@ decorator. Use with care. h2. What's In A Unit Test Writing unit tests is way outside the scope of this document, but you should read up on using nosetests, testunit, and you should look at "lamson.testing":http://lamsonproject.org/docs/api/lamson.testing-module.html for a bunch of helper functions. Also look in the generated @tests@ directory to see some examples. h2. Spell Checking Your Email Templates Another big help is that Lamson has support for "PyEnchant":http://www.rfk.id.au/software/pyenchant/ so you can spell check your templates. You can use "lamson.testing.spelling":http://lamsonproject.org/docs/api/lamson.testing-module.html#spelling function in your unit tests. Installing PyEnchant is kind of a pain, but the trick is to get the dictionary you want and put it in your @~/.enchant/myspell@ directory. You'll also want to open the @config/testing.py@ file and uncomment the lines at the bottom that tell PyEnchant where to find the enchant so (dylib). PyEnchant is kind of hard to use, so if you have suggestions on a better Python spell checking lib for unit tests please let "me know.":/contact.html h2. Spam Filtering For Free Lamson comes with the "lamson.spam":http://lamsonproject.org/docs/api/lamson.spam-module.html module which supports "SpamBayes":http://spambayes.sourceforge.net/ spam filtering system. Read the document on "Filtering Spam With Lamson":/docs/filtering_spam.html to get a full set of instructions on using the spam filtering features. h2. Other Examples Next you'll want to sink your teeth in a bigger example. Go grab "the source distribution .tar.gz":/releases/ and extract it so you can get at the examples:
$ tar -xzvf lamson-VERSION.tar.gz
$ cd lamson-VERSION
$ cd examples/osb
You are now in the osb example that is running on "oneshotblog.com":http://oneshotblog.com/. Using what you've learned so far you can start reviewing the code and finding out how a working example operates. h2. Getting Help As you work through this documentation, send your questions "to me":/contact.html and I'll try to help you. You can also join the "lamson@librelist.com mailing list":mailto:lamson@librelist.com and get help from other Lamson users. lamson-1.0pre11/doc/lamsonproject.org/input/docs/hooking_into_django.txt0000644000076500000240000000523511243575422026132 0ustar zedshawstaffTitle: Hooking Lamson Into Django This is a short document because using "Django":http://www.djangoproject.com/ ORM is very simple from Lamson. The trick is to fake out Django so that when you import the Django model into Lamson, the Django model knows where it's living and will operate. We'll go through an "integration" step by step. h1. Step 1: Make Your Django App Work There's no point in trying to import an ORM that doesn't work. So get it working, maybe write some tests. An important thing is that you should be able to run @python manage.py shell@ and import your model without problems. h2. Step 2: Fake Out Django You then have to put an environment variable in your Lamson @config/settings.py@ file before you load any Django Models:
os.environ['DJANGO_SETTINGS_MODULE'] = 'webapp.settings'
This is from the "librelist.com":http://librelist.com/ examples, where the Django models are in @webapp@ so we our settings module from Lamson perspective is @webapp.settings@. h2. Step 3: Use The Django Models After that, you can just import your models however you want. Here's an example of Librelist using a Django Model to store confirmations:
from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()
h2. Well, That Wasn't Too Hard The only thing you'll have to contend with is where code you need to use these models lives. Since it's all Python, you can just import what you need, but my recommendation is to focus most of your model code into your Django application, and then only put a small amount into Lamson. For more information, look in the "Lamson source releases":/releases/ to see how Django is used in the @examples/librelist@ code. lamson-1.0pre11/doc/lamsonproject.org/input/docs/html_email_generation.txt0000644000076500000240000001413711243706470026447 0ustar zedshawstaffTitle: HTML Email Generation HTML Email is apparently the killer feature for everyone who uses email. The first, second, and third question I get asked when I tell people about Lamson is, "Does it do HTML email?!" Yes it does, and hopefully in a very nice clean way that'll make blasting out all that great marketing material your users *love* easier than ever. bq. The fourth question is whether Lamson handles bounces. "Yes it does.":/docs/bounce_detection.html h2. A Few Tips About HTML Email First, you *must* educate your marketing people that your user's "Inbox is not a TV.":http://myinboxisnota.tv/ I actually run "myinboxisnota.tv":http://myinboxisnota.tv/ just to make a point that people who do marketing have the wrong idea of people's Inbox experience. It's your job as the hacker to educate them and make sure that they know users actually mostly hate HTML email, and that sending them an HTML email is nothing like sending them a commercial on TV. The next tip is a technical one related to how you have to craft your HTML. Lamson's "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html API actually helps you get this right, but you need to understand it in order to appreciate what it does for you. The rule when crafting your HTML is that you need to code for browsers of circa 1995 with no ability to use @style@ tags or @script@ tags. Most of the various HTML viewing clients strip or disable these making them useless. This means you have to put all CSS stylings inline into your HTML, and you have to rely on "taboo" tags like @center@ and @table@ for your layout. h2. The Structure Of HtmlMail The "lamson.html.HtmlMail":http://lamsonproject.org/docs/api/lamson.html.HtmlMail-class.html class is responsible for generating all your HTML emails. How it works is you give construct one with an "outer template" and a "CleverCSS":http://sandbox.pocoo.org/clevercss/ template as your CSS stylesheet. This builds a generator that you then hand an internal version of your email to generate the content for each user. This combination of outer template+CleverCSS and inner markdown content means that you can use the markdown content also as your text version, since markdown actually looks half-decent as a text format. Here's a simple example that shows this process in action:
generator = html.HtmlMail("style.css", "html_test.html", {})

resp = generator.respond({}, "content.markdown",
                       From="zedshaw@zedshaw.com",
                       To="zedshaw@zedshaw.com",
                       Subject="Test of an HTML mail.",
                       Body="content.markdown")
First, we make a generator passing in the CleverCSS stylesheet @style.css@, and the Jinja2 HTML template @html_test.html@. Then we call @HtmlMail.respond@ to craft a "lamson.mail.MailResponse":http://lamsonproject.org/docs/api/lamson.mail.MailResponse-class.html that you can then hand to "lamson.server.Relay":http://lamsonproject.org/docs/api/lamson.server.Relay-class.html for delivery. There's also a couple of tricks in the above email. First, notice that the second parameter is "content.markdown", but that we also pass in the Body="content.markdown". This little convenience tells the HtmlMail object that you want to reuse the raw @content.markdown@ file as the text/plain version of the email. Finally, once you make the generator you can keep calling @respond@ to spit out each message you want. h2. The HTML CSS Conversion None of the above code shows you what is the nicest part of this API. The lamson.html API will actually take plain HTML and your CleverCSS and insert the @style@ attributes into it for you. Let's look at an example from the unit test (that's disgusting). First we have the CleverCSS template:
body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h3:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black
Then we have the raw original HTML:
<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h3 id="dull">All done.</h3>
    </body>
</html>
Notice this is a template too, and that {{ content }} is your @content.markdown@ file from the earlier discussion. Now when you run this (including the content.markdown not shown here), Lamson produces this:
<html>
<head>
<title></title>
</head>
<body style="background: magenta; margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="font-size: 3em; background: black; color: white"></h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="font-size: 2em; color: yellow; color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h3 id="dull" style="font-size: 1em; background: gray; color: black">All done.</h3>
</body>
</html>
Which, if you code for a Web 2.0 company is probably making your eyes bleed Dijon mustard, but it works. Lamson has walked your HTML and inserted all the style tags it could, including keeping any you already had there. h2. Conclusion With Lamson HTML email API you should be able to blast out all the wonderful HTML you need to prop up your sales needs for years to come. It does the best it can to make it easy to still work in a modern web methodology, but produce the nasty HTML that has the highest chance of working in most email clients. lamson-1.0pre11/doc/lamsonproject.org/input/docs/index.txt0000644000076500000240000000627711243570044023232 0ustar zedshawstaffTitle: Lamson Project Documentation This is the Lamson documentation, organized into categories from "newbie" to "expert". You should also check out the "screen casts":/videos/ available which might help you if you're the more mediated type. The documentation is only updated after major releases, and it may not have the best information. If you are following the docs and they're wrong, then "send an email to lamson@librelist.com":mailto:lamson@librelist.com and report it. Finally, every document is also uploaded with it's text version. h2. Initial Concepts * "Frequently Asked Question":/docs/faq.html -- Questions people have asked about Lamson. * "Getting Started":/docs/getting_started.html -- A fast tour of getting Lamson going and doing something with it. * "Lamson Commands":/docs/lamson_commands.html -- All of the commands Lamson supports. You can get at this with @lamson -help@. * "Introduction To Finite State Machines":/docs/introduction_to_finite_state_machines.html -- Important to understand the simplified version of Finite State Machines Lamson uses. h2. Advanced Concepts * "Deferred Processing To Queues":/docs/deferred_processing_to_queues.html -- Very handy way of processing mail. * "Writing A Custom State Store":/docs/writing_a_state_storage.html -- You'll need this if you want to store state in the database. * "Primary Vs. Secondary Registration":/docs/primary_vs_secondary_registration.html -- The concept of doing registration in Lamson, where contacting the service the first time is the registration. * "Hooking Into Django":/docs/hooking_into_django.html -- Shows you how to get access to a Django ORM model. h2. Deployment * "Deploying Lamson Level 0 With Virtualenv And Pip":/docs/lamson_virtual_env.html -- Quick instructions for setting up your Lamson in a virtualenv for your first simple deployment. * "Deploying Lamson Level 1":/docs/deploying_lamson.html -- This is for when you're getting more serious about deployment. Involves building a completely separate virtualenv+python for Lamson and shows deploying oneshotblog in it. * "Deploying Lamson Level 2":/docs/deploying_lamson_level_2.html -- At this level of deployment you are running multiple sites on the same server using a virtualhost setup, and you have each application installed under its own user. h2. Deployment: Examples * "Deploying OneShotBlog":/docs/deploying_oneshotblog.html h2. Specific Features * "Unit Testing":/docs/unit_testing.html -- Lamson has a few simple things to help write better mail specific unit tests. * "Confirmations":/docs/confirmations.html -- Confirming a user so that you validate they are an actual email address. * "Filtering Spam":/docs/filtering_spam.html -- How to use Lamson's spam blocking features. It's easy to use, but a bit hairy to setup. * "Bounce Detection":/docs/bounce_detection.html -- Using Lamson's bounce message parser to handle bounces. * "Unicode Encoding And Decoding":/docs/unicode_encoding_and_decoding.html -- How Lamson decodes the nastiest email into Unicode, and then converts Unicode back into a clean email for sending. * "HTML Email Generation":/docs/html_email_generation.html -- Using Lamson's HTML email generation library to send out HTML to annoy everyone with. lamson-1.0pre11/doc/lamsonproject.org/input/docs/introduction_to_finite_state_machines.txt0000644000076500000240000002545111242713711031745 0ustar zedshawstaffTitle: A Painless Introduction To Finite State Machines Lamson uses the concept of a Finite State Machine to do the internal processing and keep track of what it should do next. You don't necessarily need to know the details of how a FSM works, but it helps you if you want to know enough about Lamson to do advanced work. Most people could be blissfully unaware of state machines and still do plenty of work with Lamson. h2. Your Computer Science Class Sucked When I say "finite state machine" everyone reads it as: bq. "FINITE STATE MACHINE!!!!! OMGWTFBBQ THOSE ARE HARD!!!!" Yes, the way your professor explained FSM makes them much harder than they are in practice. You were probably thrown fairly random sounding terms like "edge", "node", "transitions", "acceptors", "recognizers", and "transducers". You were probably shown graphs with lines and circles and told confusingly that the lines were edges and the circles were nodes, but that the edges were states unless you used this kind where the edges were transitions and that kind where the circles were states. Alright, just forget about that because they aren't really that complicated. A practical finite state machine is basically four things: * A bunch of functions, or things that need to get done. * A bunch of events, or reasons to call these functions. * Some piece of data that tracks the "state" this bunch of functions is in. * Code inside the functions that says how to "transition" or "change" into the next state for further processing. That is really all there is to every FSM. Sure you can change around what a state is, add code that runs on a transition, or make FSM that "inherit" from other FSM, but in the end they are all: bq. functions, events, states, transitions That is all there is to it, no matter how you slice these or dice these up. h2. Implementing A Practical FSM Now that we've brought the fear level down, let's actually make one. This will be a little toy FSM, but it will get you started and it won't be too far off from what Lamson uses. We are going to implement your classic email confirmation system. This system usually works like this: # You receive an email at some service, let's say users-subscribe@test.com. # You reply to the sender with a confirmation message. # They reply with an email to your confirmation's random address. # You receive this, approve the handshake, and make them subscribed with a welcome email. # They send an email, and since they are subscribed, you post it to the list. This is a typical operation on most mailing lists, but with a state machine it turns out to be fairly trivial to implement. First, we need to look at the above conversation and think about what is more clear: states the user will be in, or events that they will generate. In this case I'd go with the states they will be in during the conversation: * START -- Every machine has one of these. * PENDING -- We sent the confirm, and waiting for their reply. * POSTING -- They replied to the confirm and can now post. Notice I gave each state a verb. You are saying what state this user is in. "They are currently posting." You don't say what they "did". Now, sometimes it just makes sense to do it differently, so don't be a slave to this setup. I've just found it helps keep things consistent if you name the states after what they are currently doing. Now that we know what kind of states we're dealing with we need to solve the next two parts of the puzzle: events or transitions. In an email application I like to start with the events, as these are the email addresses and messages they will be sending me. What we do now is for each state, list the addresses we'll consider an "event" for that state. * START -- (list_name)-subscribe@test.com * PENDING -- (list_name)-confirm-(id)@test.com * POSTING -- (list_name)@test.com This is all the "events" we need right now, written out as email addresses. bq. If we were to add the ability to unsubscribe, then we would add an event for (list_name)-unsubscribe@test.com to POSTING so that we could transition them from POSTING to say, SLEEPING. We now have our states, and the events that each state answers. Last step is to figure out what state "transitions" to which other state. h2. Write The Functions At this point, our FSM is simple enough that we could just write the functions, and the logic of each function would dictate the transitions. However, if you had a larger number of states and events you would want to sit down and draw a diagram or a make a table of the transitions before you wrote some code. To start our functions we'll just name them after the states and put the events they handle at the top as pseudo code @event@ decorators:
@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    ...

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    ...

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    ...
This is abbreviating the syntax quite a bit, is not functioning Python code but it is pretty close. Instead of @event@ you would have a @route@ decorator and it would have a few regexes. Otherwise it's about the same. bq. We could also say that each state handles multiple events, which is what you would do if POSTING handled the "unsubscribe" requests. h2. Add Logic And Transitions The final piece, and the part you'll spend the longest getting right, is filling in the logic and making the transitions happen. How would you indicate *where* each state should go next? Remember that each state is a simple Python function, and that to "transition" means to change to another state. Well, we have to tell whatever is running the state machine to run a *different* function next time. Easiest way to do that is for our handlers to just return the next function. The "runner" will then take that, store it somewhere, and the next time an event comes the runner will load the next function to run. Here's the psuedo code to do just that:
@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    send_confirmation()
    return PENDING

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    if check_confirmation_is_good():
        send_welcome()
        return POSTING
    else:
        ignore_them()

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    deliver_message_to_all()
    return POSTING
Right away you can see that we change to the next state by just returning the actual function to call next. When the next event comes in, the runner matches it to the right function, calls it, and then sets up for the next one. If you look at PENDING, you can see that it either returns POSTING if they confirm correctly or it just ignores them. You could also transition to "return ERROR" if you wanted to put them in an error state and send a different message. Looking at POSTING, you see that it just keeps returning itself to indicate that it is staying there. If you had POSTING process "unsubscribes" then it would simply do the unsubscribe confirm, and then transition to UNSUBSCRIBING. That state function would then check the confirmation and unsubscribe them, transitioning to something like DEAD or SLEEPING. bq. An "optimization" in Lamson is that if a state function doesn't return anything then it's assumed to just want to stay in that state. In this case POSTING could have no return and it would work the same. h2. Jump To vs. Transition You now know pretty much everything you need to handle FSM except for a tiny corner case. It is typically called the "epsilon transition" which basically means "transition to that state without an explicit event". When you use this is when your event needs to fire off the code for the *next* transition right now, rather than waiting for the next event from the user. In the above pseudo code this is simply done by actually calling that transition function and returning whatever it returns. Let's say we wanted to have PENDING also call POSTING with the original message they sent:
def PENDING(message):
    if check_confirmation_is_good():
        send_welcome()
        return POSTING (message):
    else:
        ignore_them()

def POSTING(message):
    deliver_message_to_all()
    return POSTING
Notice that we changed the @return POSTING@ to a @return POSTING(...)@ which returns the results of calling POSTING. That's it, you now know epsilon transitions. Man, that was tough. bq. The danger of this is if you don't have your FSM carefully mapped out, then you could call a state that loops back to your state and you're in an endless loop. Watch for that. h2. Why Finite State Machines Anyway Finite State Machines like this are very powerful because they behave in ways that are consistent, easily debugged, and intelligent. Because the decision of how each step in the chain of events is controlled by a constrained set of states, events, and transitions, you can actually avoid many bugs you'd get in regular classes and objects. For example, if you were to receive another subscribe message while there is a confirmation pending, then that event (email address) isn't recognize and just ignored. You would see it in the logs, and see that your FSM stayed in the PENDING state. If you then wanted PENDING to handle additional requests it's a simple matter of adding that event and writing the code. The way to think of the a FSM is it is like an object that has a white list of functions, parameters, AND allowed values for its private data. If an FSM tries to change to a state that doesn't exist it's an error. If it gets an event it doesn't know it ignores it. If you try to transition wrong you see it in the logs, or it's an error. FSM also have the debugging advantage of showing you the history of not only what states were called, but *why* they were called and what they did next. You will see the entire conversation and can pinpoint exactly where it went wrong. However, the most important reason to use FSM is because this is how email and asynchronous conversations work. When you have a conversation with someone there is state involved. You don't have to start the entire conversation from scratch at the start of each sentence. Instead you remember what the person said and what state you are in (angry, sad, happy) and that controls what you do next (punch them, run away, hug them) based on the events they send ("screw you", "you're dead", "i love you"). The use of an FSM will make your Lamson applications seem like magic. They will behave more like smart systems that just seems to know like a wizard what should happen next, and *why* it should happen that way. lamson-1.0pre11/doc/lamsonproject.org/input/docs/lamson_commands.txt0000644000076500000240000001206411242706605025267 0ustar zedshawstaffTitle: Available Lamson Commands Content-Type: text/html

The following is also available by running lamson help and you can get the help for each individual command with lamson help -for COMMAND replacing COMMAND with one of these listed below.

The format for the printed options show default options as an actual setting, and required options as a CAPITALIZED setting you must give. For example, in the send command:

lamson send -port 8825 -host 127.0.0.1 -debug 1 -sender EMAIL -to EMAIL -subject STR -body STR -attach False

The options -port, -host, -debug, and -file have default settings, but -sender, -to, -subject and -body require a STRing or EMAIL. Notice also that -file defaults to False which you can change by just including -file (that toggles it true).

lamson blast

Given a maildir, this command will go through each email
and blast it at your server.  It does nothing to the message, so
it will be real messages hitting your server, not cleansed ones.

lamson cleanse

Uses Lamson mail cleansing and canonicalization system to take an
input maildir (or mbox) and replicate the email over into another
maildir.  It's used mostly for testing and cleaning.

lamson gen

Generates various useful things for you to get you started.

lamson gen -project STR -FORCE False

lamson help

Prints out help for the commands. 

lamson help

You can get help for one command with:

lamson help -for STR

lamson log

Runs a logging only server on the given hosts and port.  It logs
each message it receives and also stores it to the run/queue 
so that you can make sure it was received in testing.

lamson log -port 8825 -host 127.0.0.1 \
        -pid ./run/log.pid -chroot False  \
        -chdir "." -umask False -uid False -gid False \
        -FORCE False

If you specify a uid/gid then this means you want to first change to
root, set everything up, and then drop to that UID/GID combination.
This is typically so you can bind to port 25 and then become "safe"
to continue operating as a non-root user.

If you give one or the other, this it will just change to that
uid or gid without doing the priv drop operation.

lamson queue

Let's you do most of the operations available to a queue.

lamson queue (-pop | -get | -remove | -count | -clear | -keys) -name run/queue

lamson restart

Simply attempts a stop and then a start command.  All options for both
apply to restart.  See stop and start for options available.

lamson routes

Prints out valuable information about an application's routing configuration
after everything is loaded and ready to go.  Helps debug problems with
messages not getting to your handlers.  Path has the search paths you want
separated by a ':' character, and it's added to the sys.path.

lamson routes -path $PWD -- config.testing -test ""

It defaults to running your config.testing to load the routes. 
If you want it to run the config.boot then give that instead:

lamson routes -- config.boot

You can also test a potential target by doing -test EMAIL.

lamson send

Sends an email to someone as a test message.
See the sendmail command for a sendmail replacement.

lamson send -port 8825 -host 127.0.0.1 -debug 1 \
        -sender EMAIL -to EMAIL -subject STR -body STR -attach False'

lamson sendmail

Used as a testing sendmail replacement for use in programs
like mutt as an MTA.  It reads the email to send on the stdin
and then delivers it based on the port and host settings.

lamson sendmail -port 8825 -host 127.0.0.1 -debug 0 -- [recipients]

lamson start

Runs a lamson server out of the current directory:

lamson start -pid ./run/smtp.pid -FORCE False -chroot False -chdir "." \
        -umask False -uid False -gid False -boot config.boot

lamson status

Prints out status information about lamson useful for finding out if it's
running and where.

lamson status -pid ./run/smtp.pid

lamson stop

Stops a running lamson server.  Give -KILL True to have it
stopped violently.  The PID file is removed after the 
signal is sent.  Give -ALL the name of a run directory and
it will stop all pid files it finds there.

lamson stop -pid ./run/smtp.pid -KILL False -ALL False

lamson version

    Prints the version of Lamson, the reporitory revision, and the
    file it came from.

lamson web

Starts a very simple files only web server for easy testing of applications
that need to make some HTML files as the result of their operation.
If you need more than this then use a real web server.

lamson web -basedir "." -port 8888 -host '127.0.0.1'

This command doesn't exit so you can view the logs it prints out.
lamson-1.0pre11/doc/lamsonproject.org/input/docs/lamson_virtual_env.txt0000600000076500000240000000315511242705742026016 0ustar zedshawstaffTitle: Installing Lamson Into Virtualenv With Pip The best way to install Lamson in a virtualenv is using the "pip tool":http://pip.openplans.org/ to install it. This will automatically fetch the code from bazaar, install it inside the virtualenv and fetch any missing dependencies. First, if you haven't already, you'll need to install pip. Make sure you've activated the virtualenv:
. bin/activate
from the root of the virtualenv, and then run:
easy_install -U pip
If you have bazaar installed on your machine, then just do:
pip install -e bzr+http://bazaar.launchpad.net/~zedshaw/lamson/development/#egg lamson
This'll install the development version of lamson into a src/lamson directory (src/ will be created if it doesn't exist). It will also install all of Lamson's dependencies into the virtualenv. If you want to update to the latest development version of Lamson, run:
cd src/lamson
bzr pull
This will fetch the latest bazaar tip from Launchpad. If you don't have bazaar installed and don't want to install it, you can grab the latest release of Lamson like this:
pip install -F http://launchpad.net/lamson/trunk/0.9-pre2/+download/lamson-0.9-pre2.tar.gz
Remember though that if you do install it like this you'll need to manually update it by visiting Launchpad, finding the latest tarball URL and `pip install`ing it. h2. Credits These instructions were donated to Lamson by "Zachary Voase":http://disturbyte.github.com but "contact me":/contact.html for suggested improvements. lamson-1.0pre11/doc/lamsonproject.org/input/docs/primary_vs_secondary_registration.txt0000644000076500000240000001575611242714123031156 0ustar zedshawstaffTitle: Primary vs. Secondary Registration When you design a Lamson application the question of "registration" comes up almost immediately. The question is one of how you find out who a person is so that you can interact with them. In the web world this involves a form of some kind asking for a login and password at a minimum. Some sites even go so far as to require a sign-up process *before* they let you see anything about the site, or even their marketing material. On the web this kind of registration is what I like to call "Primary Registration". It comes from the concept of primary vs. secondary data, or primary vs. secondary analysis. In the world of measurement a primary source is one that you gathered the information from directly, usually by giving them a questionaire. A secondary source is one that you gathered in some indirect way, either by evaluating past research in a new way, data mining, or simply unobtrusive data collection. With a web site you *must* have a primary registration system. There is no implied identity in a browser that is consistent for a user, so you need to ask them for some information and associate their stateless browser to a cookie or similar tracking mechanism. You have to do this for each session you start with them, and to register them if your service requires some kind of password protection. In the world of the web primary registration is just how things are done. h2. Secondary Registration Lamson give you the ability to do *Secondary Registration* where the act of interacting with your service *is* the registration. A typical primary registrationg process will ask for a user's email address to identify the user, but in Lamson the user actively gives your service their email address to start playing. Their first message to your service actually has all the information you need to sign them up and give them their first taste of your service. In fact, a user's identity is so baked into the email protocols that your Lamson server has almost the inverse problem: it's too easy to fake an identity, so you need to confirm their subscription and important actions. This confirmation is traditionally nothing more than a reply asking them to reply with some random number in the address. If you get a reply to this auto-generated email address then you assume they are real. bq. This "random number confirmation handshake" is important because SMTP allows anyone with an internet connection to lie about who they are and craft a fake email claiming to be someone else. The confirmation assures you that the supposed sender has at least received an email in their inbox and that it had the random number you generated in the reply address. A determined hacker could also get past that, but if you have that situation you are dealing with more problems than just confirmations. Once you do though you are fairly assured that they are identified *and* using your service. It ends up being the most low effort entry to your service you could possibly devise without making the service unsafe. h2. Comparing Two Designs When you sit down to build a Lamson application you may still think about registration in the primary way, but a secondary registration is typically more natural for an email service. To illustrate the point, let's say you are creating an online game using Lamson, and you want people to register for the game. With a primary registration you would approach the problem by making your game open only to registered users. If they send an email to your game you send a single reply pointing them at the web page they can use to sign up. You also then take every email and pass it through a filter that checks that the user is registered in your subscription database. This is mostly how you would do it in a web application. bq. Maybe there is a legitimate reason to force a user to a web page to give a password and other information before using the service. If that's the case, then go for it, but consider trying to make the *entry* to your service a secondary registration that then asks them to do a full registration to get more features. With a secondary registration system, you are using Lamson's ability to track the state of every user to know whether they are registered or not. When your game receives their first email, it would transition to an @UNKNOWN@ state and reply with a confirmation email. Once they reply to the confirm email you transition from @UNKNOWN@ to the rest of the game since they are now registered. Every interaction after that will simply know they are registered because they are in the right state. This has a big impact on your game's design because now you do *not* need to check the database on every received email to confirm their identity. Their email address and their state in your state's storage tells you that implicitly. You can also extend this to other requirements for your system. You can use their known state to transition them from free to paid quality services, to suspended states, to pause them after a bounce, and back to normal activity. h2. Invite Only Systems An invite only system can also use the secondary registration technique by having the inviter's invitation request create a record for the invitee that's in the right start state. When the inviter tells you to invite their friend, you create a state for the friend that says @INVITED@ and use the invite as a sort of implied confirmation email. When the invitee replies to this invite email, you transition them to the rest of the system as they've now registered. Another way to think of an invite only system in Lamson is as a 3rd-party confirmation. Rather than the target user (the invitee) prompting a confirmation, it is their friend sending the invite (the inviter) who triggers the confirmation process on behalf of their buddy. An additional bonus with Lamson is that the state of the invitee is known, so if the inviter tries to send them again, or if someone else does, then you know they already were sent an invite. You can just ignore the request and avoid spamming people. You can also run reports to find out your conversion rates by simply looking for everyone in the various states and counting. h2. Security And Design Issues Doing these kind of secondary registrations and implied identity does come with an extra security penalty. Unless your system is incorporating S-MIME or GPG you won't have a solid way to identify the sender. You don't have this on most other internet services either so it's not much of a loss. At least with email you have a higher assurance that this is a real person. When you design your service it's a good idea to: # Either make everything the user does non-destructive, # Send confirmations for anything desctructive, # Or bounce them to a web site for more complex interactions and security. You should also make any confirmations you send easily replied to, but have a good random number in them that you remember for later. In fact, if you use the "lamson.confirm" API you get all of this for free and done the right way. lamson-1.0pre11/doc/lamsonproject.org/input/docs/unicode_encoding_and_decoding.txt0000644000076500000240000001443111243702515030064 0ustar zedshawstaffTitle: Unicode Encoding And Decoding The world is Unicode, but email existed long before that world. Lamson uses special encoding/decoding gear that takes just about any nasty horrible pre-Unicode email it gets and converts it to a perfect little Unicode fantasy. You do all your work in the Unicode world, which Python favors, and then when you're ready to send, Lamson intelligently figures out how to make an email that anyone can read. The added advantage of doing this conversion is that Lamson by default also cleans up all the various tricks spammers use to get around spam checkers. Because it's doing a conversion from just first principles of "everything must become Unicode" the end result is a crystal clear piece of data you can filter. When you then output the same message, it gets reconverted to sane easily parseable message. Lamson is also so persistent in this conversion that it can convert nearly every message that's valid, and the very tiny percentage it can't (less 1/1000th of a percent) are entirely screwed up spam that should not be transmitted anyway. bq. This gear apparently doesn't support pre-Hindic sanskrit, but then again neither does Python. h2. Why Sometimes people ask why Lamson would bother converting everything to Unicode? The reason is simple: everything you deal with now is Unicode. Either it's in a representation like UTF-8, or it's internally a Python Unicode string. Databases, web frameworks, ORM, network protocols, and the entire internet is Unicode. However, there's another practical reason. If you were to start using Lamson to process email, you would end up inventing almost the exact same gear to convert headers and bodies to Unicode. The only difference is you would do it in a typical hacker half-assed fashion that only solved your immediate problems, and you wouldn't do it in a global useful way. The Lamson encoding gear is basically what you should be making to deal with email in a modern language. h2. How The "lamson.encoding":http://lamsonproject.org/docs/api/lamson.encoding-module.html module is the main meat of this conversion magic. This code is probably the most dense part of Lamson since it has to use various parsing tricks and heuristics to figure out how to get every part of the message into a Unicode string. The primary trick used is that Lamson does not trust anything it's given, and when python fails to convert a header or body, it uses the wonderful "chardet":http://chardet.feedparser.org/ library to guess at what the encoding should be based on the contents. If this fails then the entire data is considered suspect and the message is rejected. This turns out to be surprisingly accurate in practice, and it even corrects some invalid clients that fail to encode Subject lines but still place Chinese encodings in them. Lamson will take those unencoded headers, fail to convert them to ascii, run chardet on them to see they are actually Chinese, and then convert them accurately. h2. The Rules A set of axioms controls how the encoding is done: # NO ENCODING IS TRUSTED, NO LANGUAGE IS SACRED, ALL ARE SUSPECT. # Python wants Unicode, it will get Unicode. # Any email that CANNOT become Unicode, CANNOT be processed by Lamson or Python. # Email addresses are ESSENTIAL to Lamson's routing and security, and therefore will be canonicalized and properly encoded. # Lamson will therefore try to "upgrade" all email it receives to Unicode internally, and cleaning all email addresses. # It does this by decoding all codecs, and if the codec LIES, then it will attempt to statistically detect the codec using chardet. # If it can't detect the codec, and the codec lies, then the email is bad. # All text bodies and attachments are then converted to Python Unicode in the same way as the headers. # All other attachments are converted to raw strings as-is. Once Lamson has done this, your Python handler can now assume that all MailRequest objects are happily Unicode enabled and ready to go. The rule is: IF IT CANNOT BE UNICODE, THEN PYTHON CANNOT WORK WITH IT. On the outgoing end (when you send a MailResponse), Lamson tries to create the email it wants to receive by canonicalizing it: # All email will be encoded in the simplest cleanest way possible without losing information. # All headers are converted to 'ascii', and if that doesn't work, then 'utf-8'. # All text/* attachments and bodies are converted to ascii, and if that doesn't work, 'utf-8'. # All other attachments are left alone. # All email addresses are normalized and encoded if they have not been already. h2. Neat Tricks The end result of this is that you can now take any email and then convert it to any modern data format and send it over new protocols. For example, here's the code from "librelist.com":http://librelist.com/ to implement the JSON dump for the "archive browser":http://librelist.com/browser/ Javascript interface:
def json_encoding(base):
    ctype, ctp = base.content_encoding['Content-Type']
    cdisp, cdp = base.content_encoding['Content-Disposition']
    ctype = ctype or "text/plain"
    filename = ctp.get('name',None) or cdp.get('filename', None)

    if ctype.startswith('text') or ctype.startswith('message'):
        encoding = None
    else:
        encoding = "base64"

    return {'filename': filename, 'type': ctype, 'disposition': cdisp,
            'format': encoding}

def json_build(base):
    data = {'headers': base.headers,
                'body': base.body,
                'encoding': json_encoding(base),
                'parts': [json_build(p) for p in base.parts],
            }

    if data['encoding']['format'] and base.body:
        data['body'] = base64.b64encode(base.body)

    return data

def to_json(base):
    return json.dumps(json_build(base), sort_keys=True, indent=4)
Since this code can assume that everything inside Lamson is fully Unicode, it only needs to worry about binary items like images and encode those as base64. h2. lamson cleanse You can also use the @lamson cleanse@ command to take a Maildir or mbox as input, and then convert it into a cleaned up Maildir as output. Try it on some of your spam folders to watch the magic. h2. Reporting Problems If you happen to run into a message that you feel Lamson should accurately decode, feel free to "report it":/contact.html and I'll take a look. lamson-1.0pre11/doc/lamsonproject.org/input/docs/unit_testing.txt0000644000076500000240000002525111243070347024631 0ustar zedshawstaffTitle: Unit Testing Lamson provides the "lamson.testing":http://lamsonproject.org/docs/api/lamson.testing-module.html to help with writing unit tests. It doesn't do everything for you, but does enough that you can TDD your email interactions with pretend users. It includes features for checking messages get delivered to queues, checking spelling, and running things through a fake or real relay. h2. The Log Server The first thing you need to do for testing is to run the "log server":
$ lamson log
The log server acts as the smart relay host you've configured in your @config/settings.py@ file by default. What it does is take all emails that your Lamson application "sends out" and redeposits them into @run/queue@. This queue directory is then also used by @lamson.testing@ for checking that emails were sent out. When anything goes wrong, you can look in this directory and see what is getting sent out. bq. An alternative to this setup would be to have all of the sending/routing/relaying done internal to the whole testing framework, similar to how @lamson.testing.RelayConversation@ works. However, I found that this made testing that your server actually sends proper emails much too difficult. h2. Test Organization Lamson organizes tests into directories that match the things you're testing:
librelist $ ls -l tests/
total 16
drwxr-xr-x  6 zedshaw  staff   204 Aug 19 17:01 handlers
drwxr-xr-x  8 zedshaw  staff   272 Aug 18 15:50 model
drwxr-xr-x  3 zedshaw  staff   102 Aug 18 15:50 templates
In most of the projects I've only rarely used template tests, but I'll cover them below. Model tests make sure that any @app.model@ classes work right, and handler tests make sure that any @app.handler@ classes work. Nice and simple. You can add any other directories you want to this, and you can also use doctests if you want. This comes free with "nosetests":http://somethingaboutorange.com/mrl/projects/nose/0.11.1/ and is very handy. h2. Handler Tests We'll use an example from the "librelist.com":http://librelist.com/ code base that validates that a user can subscribe, unsubscribe, and post a message to a mailing list. This test was primarily written in a TDD style, since generally interactions and usability testing works better "TDD":http://en.wikipedia.org/wiki/Test-driven_development style. First you have a common preamble of modules that you need to include:
from nose.tools import *
from lamson.testing import *
from config import settings
import time
from app.model import archive, confirmation

Right away you notice we just include everything from @nose.tools@ and @lamson.testing@ so that we can use it directly. Yes, this violates Python style guidelines, but practicality is more important than dogmatic slavery to supposed standards. After that we include the @config.settings@, and two modules from @app.model@ that we'll use to check that everything was working. bq. Notice that we don't include anything from @app.handlers@ directly. These tests are meant to be from the perspective of a user interacting with the handler via emails. Once we have that we do a little setup to clear set some common variables and clear out some queues we'll need to check:
queue_path = archive.store_path('test.list', 'queue')
sender = "sender-%s@sender.com" % time.time()
host = "librelist.com"
list_name = "test.list"
list_addr = "test.list@%s" % host
client = RouterConversation(sender, 'Admin Tests')


def setup():
    clear_queue("run/posts")
    clear_queue("run/spam")
Most of these are just variables used in tests later, but the big one is the @client@ variable. It's a "lamson.testing.RouterConversation":http://lamsonproject.org/docs/api/lamson.testing.RouterConversation-class.html class that lets you simulate delivering email to your Lamson project. bq. There's also a "lamson.testing.TestConversation":http://lamsonproject.org/docs/api/lamson.testing.TestConversation-class.html class that actually uses your real @config/settings.py@ to connect to a Relay. This isn't used so much, but is intended for running "smoke tests" against a newly deployed server. With that we're ready to write out first handler test:
def test_new_user_subscribes_with_invalid_name():
    client.begin()

    client.say('test-list@%s' % host, "I can't read!", 'noreply')
    client.say('test=list@%s' % host, "I can't read!", 'noreply')
    clear_queue()

    client.say('unbounce@%s' % host, "I have two email addresses!")
    assert not delivered('noreply')
    assert not delivered('unbounce')

    client.say('noreply@%s' % host, "Dumb dumb.")
    assert not delivered('noreply')
This is the longest of the tests, and shows all the various things you can do with the @lamson.testing@ gear. Here's what we're doing in order: # Call @client.begin@ to clear out queues and state and start fresh. # Use @client.say@ to send an email from that client to your Lamson application. Notice that you configured the RelayConversation to pretend to be one person with each email getting the same subject line. # Use @lamson.testing.clear_queue@ when you want to make sure the queue is clean. # Use @lamson.testing.delivered@ to check if a certain message from someone is in the queue. With that you can do pretty much everything you need to send an email and make sure you get proper replies. Here's another example:
def test_new_user_subscribes():
    client.begin()
    msg = client.say(list_addr, "Hey I was wondering how to fix this?",
                     list_name + '-confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')
    clear_queue()
Notice in this example we have a fourth parameter @list_name + '-confirm'@ and we get a @msg@ back from our call to @client.say@. This basically combines @client.say@ with @delivered@ to do it in one shot. Very commonly, you'll want to say something to your server and make sure you got a certain response, and then do something with that response. This is how you do that. We then use this '-confirm' email message to actually subscribe the fake user. Finally, here's two more examples:
def test_existing_user_unsubscribes():
    test_new_user_subscribes()
    msg = client.say(list_name + "-unsubscribe@%s" % host,
        "I would like to unsubscribe.", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed yes I want out.', 'noreply')

def test_existing_user_posts_message():
    test_new_user_subscribes()
    msg = client.say(list_addr, "Howdy folks, I was wondering what this is?",
                     list_addr)
    # make sure it gets archived
    assert delivered(list_addr, to_queue=queue(queue_path))
In @test_existing_user_unsubscribes@ what we do is call @test_new_user_subscribes@ to go through that process again, and then we chain off that to do an unsubscribe. There's really nothing new here other than that little trick. In @test_existing_user_posts_message@ we do the usual send a message and expect a reply, but then we *also* make sure that this message was delivered to the archiver queue. Apart from those methods and techniques, there's really nothing more to doing a handler test. The only additional thing would be using "assert_in_state":http://lamsonproject.org/docs/api/lamson.testing-module.html#assert_in_state to make sure that your handler is in a particular state. I'd recommend against doing that too much in a handler test, since it will make your tests brittle. I only do it when the state is very important, such as when checking that they are in a SPAMMING or BOUNCING state that I need to enforce. h2. Model Tests There's less functionality available in @lamson.testing@ for doing your models. The theory is that your models will be classes, modules, and ORM that you need to perform the majority of your storage and analysis. Since has very little to do with email you probably won't use @lamson.testing@ as much. About the only things you might use are APIs for checking that queues get certain messages in them, and that certain users are in certain states. Here's a quick example from "librelist.com":http://librelist.com again that tests how archives work:
from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest, MailResponse
from app.model import archive, mailinglist
import simplejson as json
import shutil

queue_path = archive.store_path('test.list', 'queue')
json_path = archive.store_path('test.list', 'json')

def setup():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def teardown():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def test_archive_enqueue():
    msg = MailResponse(From="zedshaw@zedshaw.com", To="test.list@librelist.com",
                       Subject="test message", Body="This is a test.")

    archive.enqueue('test.list', msg)
    assert delivered('zedshaw', to_queue=queue(queue_path))
This is the usual initial setup, and then some extras to make sure that the "JSON archives":http://librelist.com/browser/ is working. You'll notice that we hand construct various messages, call methods on the @app.model.archive@ module, and then use @delivered@ to make sure they're correctly delivered. h2. Template Tests Typically you really can only test that your templates are spelled right, or that your templates render when given certain locals. I've found that automated testing of templates isn't incredibly useful yet, so the only one I've written is from the "oneshotblog":http://oneshotblog.com/ example:
from nose.tools import *
from lamson.testing import *
from lamson import view
import os
from glob import glob

def test_spelling():
    message = {}
    original = {}
    for path in glob("app/templates/mail/*.msg"):
        template = "mail/" + os.path.basename(path)
        result = view.render(locals(), template)
        spelling(template, result)
This uses @lamson.testing.spelling@ to make sure that each template renders and that it is spelled correctly. This uses "PyEnchant":http://www.rfk.id.au/software/pyenchant/ to do the checking, which turns out to be rather annoying. If you are interested in improving the template testing setup, then feel free to talk about your ideas on "the lamson mailing list":mailto:lamson@librelist.com (but bring code, talk is cheap). h2. Conclusion Hopefully you'll be able to develop your application using good testing techniques with the @lamson.testing@ API. If you find additional testing patterns that could be included then "talk about them on the lamson mailing list":mailto:lamson@librelist.com to see if they're general enough for others. lamson-1.0pre11/doc/lamsonproject.org/input/docs/writing_a_state_storage.txt0000644000076500000240000002146611242714313027025 0ustar zedshawstaffTitle: Writing A Lamson State Storage Backend Earlier versions of Lamson assumed that you would use SQLAlchemy, and that you wouldn't mind storing your state in the database using SQLAlchemy. Well, during the 0.9 redesign it became very easy to let you store the state however and wherever you want. In the new Lamson setup there is a bit more work to create alternative storage, but Lamson comes with two default stores that you can use to get started. h2. The Default MemoryStore When you get started with Lamson you'll definitely not want to go through the trouble of setting up a custom store. For your first days of development, using the default "MemoryStorage":http://lamsonproject.org/docs/api/lamson.routing.MemoryStorage-class.html is the way to go. After you get further in your development you want to switch to the "ShelveStorage":http://lamsonproject.org/docs/api/lamson.routing.ShelveStorage-class.html to store the state in a simple Python "shelve":http://docs.python.org/library/shelve.html store. @MemoryStorage@ keeps the routing state in a simple dict in memory, and doesn't provide any thread protection. Its purpose is for developer testing and unit test runs where keeping the state between disks is more of a pain than it is worth. Use the @MemoryStorage@ (which is the default) for your development runs and for simple servers where the state does need to be maintained (very rare). Here's the code to MemoryStore for you to just look at, it's already included in Lamson:
class MemoryStorage(StateStorage):
    """
    The default simplified storage for the Router to hold the states.  This
    should only be used in testing, as you'll lose all your contacts and their
    states if your server shutsdown.  It is also horribly NOT thread safe.
    """
    def __init__(self):
        self.states = {}

    def get(self, module, sender):
        key = self.key(module, sender)
        try:
            return self.states[key]
        except KeyError:
            self.set(module, sender, ROUTE_FIRST_STATE)
            return ROUTE_FIRST_STATE

    def set(self, module, sender, state):
        key = self.key(module, sender)
        self.states[key] = state

    def key(self, module, sender):
        return repr([module, sender])

    def clear(self):
        self.states.clear()
As you can see there isn't much to implement to make your own storage for Lamson to use. bq. Keep in mind that this is just the storage *Lamson* needs to operate, you probably don't want to be accessing this in your own application, and instead probably want to access the store you create yourself. h2. The ShelveStorage @ShelveStorage@ is used for your small deployments where you are mostly just testing the deployment process or doing a small service. It will store your data between runs, and is probably fast enough for most sites, but you'll want to ditch it if you ever: # Run more than one process that needs the state information. # Start to store everything in a database anyway. The code to ShelveStorage (which is *already* part of Lamson) is more complex since it must keep the threads happy, but you should read through it to get an idea of how a more complex state store would work:
class ShelveStorage(MemoryStorage):
    """
    Uses Python's shelve to store the state of the Routers to disk rather than
    in memory like with MemoryStorage.  This will get you going on a small
    install if you need to persist your states (most likely), but if you 
    have a database, you'll need to write your own StateStorage that 
    uses your ORM or database to store.  Consider this an example.
    """
    def __init__(self, database_path):
        """Database path depends on the backing library use by Python's shelve."""
        self.database_path = database_path
        self.lock = threading.RLock()

    def get(self, module, sender):
        """
        This will lock the internal thread lock, and then retrieve from the
        shelf whatever module you request.  If the module is not found then it
        will set (atomically) to ROUTE_FIRST_STATE.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            try:
                key = store[self.key(module, sender)]
            except KeyError:
                self.set(module, sender, ROUTE_FIRST_STATE)
                key = ROUTE_FIRST_STATE
            return key

    def set(self, module, sender, state):
        """
        Acquires the self.lock and then sets the requested state in the shelf.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store[self.key(module, sender)] = state
            store.close()

    def clear(self):
        """
        Primarily used in the debugging/unit testing process to make sure the
        states are clear.  In production this could be a bad thing.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store.clear()
            store.close()
h2. Using The ShelveStorage You can use @ShelveStorage@ by simply adding this line to your @config/boot.py@ file just before you do anything else with the @Router@:
from lamson.routing import ShelveStorage
Router.STATE_STORE=ShelveStorage("run/states")
It actually doesn't matter currently when you do it, but it's good practice right now. After you do that, restart lamson and it will start using the new store. Notice that your *tests will not use this*. It's not a good idea to have tests use @ShelveStorage@, but if you want to turn it on for a run to see what happens, then you can modify @config/testing.py@ the same way. You could also write a unit test that did this temporarily by putting that line in your test case. h2. What To Implement If you want to implement your own then you just have to implement the methods in "StateStorage":http://lamsonproject.org/docs/api/lamson.routing.StateStorage-class.html and make sure it behaves the same as MemoryStorage. Look at the code to ShelveStorage for a moment to see what you need:
class StateStorage(object):
    """
    The base storage class you need to implement for a custom storage
    system.
    """
    def get(self, module, sender):
        """
        You must implement this so that it returns a single string
        of either the state for this combination of arguments, OR
        the ROUTE_FIRST_STATE setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.get.")

    def set(self, module, sender, state):
        """
        Set should take the given parameters and consistently set the state for 
        that combination such that when StateStorage.get is called it gives back
        the same setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.set.")

    def clear(self):
        """
        This should clear ALL states, it is only used in unit testing, so you 
        can have it raise an exception if you want to make this safer.
        """
        raise NotImplementedError("You have to implement a StateStorage.clear for unit testing to work.")
There really isn't much to it, just methods to get and set based on the module and sender's email address. Also notice that you don't have to make it readable in any complete sense, since Lamson doesn't do anything other than get, set, and clear the state store (and it only clears on reloads and in testing). h2. Important Considerations I am purposefully *not* telling you how to exactly implement it because I'm not exactly sure what is needed as of the 0.9 release. I use the the ShelveStorage, but I'd like to hear what other people have written and then start building infrastructure to make that easier. There are some important things to consider when you implement your storage though: # Make sure that the calls to all methods are thread safe, and potentially process safe. # If you do thread locking, use the *with* statement and an RLock. # If your storage is potentially very slow, then consider a caching scheme inside, but *write that after making it work correctly.* # Do *NOT* be tempted to store junk in this like it is a "session". It should be lean and mean and only do state. # Make sure you keep the key being used exactly as given. You can seriously mess up Lamson's Router if you start getting fancy. h2. Attaching It To The Router Your storage backend will then be attached to the lamson.routing.Router in the same way as what you did with ShelveStorage. It really should be that simple since the data stored in the state store is very minimal. h2. Other Examples If you want more examples then you can look at the @examples/librelist@ code to see how "librelist.com":http://librelist.com/ uses "Django":http://www.djangoproject.com/ to store the state. lamson-1.0pre11/doc/lamsonproject.org/input/download.txt0000644000076500000240000000241311227340123022761 0ustar zedshawstaffFrom: Zed Title: Downloading Lamson You can get the Lamson *source*, read "the source releases":/releases/ documentation to find out how. For people who can't click the above link, and who still want the source code, do:
bzr branch lp:lamson
But really you should read the read "the source releases":/releases/ documentation to find out how. h2. Installing With Easy Install Using easy_install is the easiest way to install, simply run "sudo easy_install lamson" and it will do the work of getting the dependencies and setting everything up. h2. Installing With setup.py Once you get the source, you can also use setup.py to install Lamson. First make sure that you have *Mako*, and *nose* installed:
$ sudo easy_install sqlalchemy
$ sudo easy_install jinja
$ sudo easy_install nose
If you refuse to use easy_install entirely then it is on you to find these projects and install them how you see best. *Make sure you have the most recent version of all dependencies.* Then you can use this command to install:
$ python setup.py install
h2. Installing Into A Virtual Env Read the instructions "for installing with virtual env":/docs/lamson_virtual_env.html in the "documentation":/docs/ section. lamson-1.0pre11/doc/lamsonproject.org/input/home_template.html0000644000076500000240000001231711230770103024125 0ustar zedshawstaff LamsonProject: $title

Features

  • Avoid aliases forever! Lamson uses friendly regular expressions and FSM-based routing.

  • Use an RDBMS and ORM instead of a bizarre combination of flat-files and hashtables!

  • Get up and running quickly. Lamson can be installed and running in 30 seconds.

Get Started »

The Python SMTP Server

We've all been there, mucking around in the sendmail m4 macros trying one more time to get the damn mailing list to update for the new users. Every time we say, "This sucks, I want to rewrite this stupid thing." Yet, when we're done, we simply crawl back to our caves covered in our sendmail wounds.

Lamson's goal is to put an end to the hell that is "e-mail application development". Rather than stay stuck in the 1970s, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

$title

$content

lamson-1.0pre11/doc/lamsonproject.org/input/index.txt0000644000076500000240000001072111227340344022267 0ustar zedshawstaffFrom: Zed Title: Lamson The Python Mail Server template: input/home_template.html Content-Type: text/html

Rather than stay stuck in the 1970's, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

@route("(post_name)@(host)")
def START(message, post_name=None, host=None):
    message['X-Post-Name'] = post_name
    confirmation.send(relay, "post", message, "mail/confirm.msg", locals())
    return CONFIRMING

Instead of hideous aliases files (that you never remember to update) Lamson uses friendly regular expressions and routing decorators that make configuring how email is handled trivial.

@route("post-confirm-(id_number)@(host)", id_number="[a-z0-9]+")
def CONFIRMING(message, id_number=None, host=None):
    original = confirmation.verify(message, id_number)
    ...


@route("(post_name)@(host)")
@route("(post_name)-(action)@(host)", action="delete")
def POSTING(message, post_name=None, host=None, action=None):
    name, address = parseaddr(message['from'])
    ...

Rather than bizarre flat file "databases" and hashtable stores, Lamson uses anything that Python does to store data. You can use all of the following to store data in Lamson:

There's so many great ways to store data in Python that Lamson doesn't make any assumptions other than to provide a convenience function or two for configuring SQLAlchemy (since it's kind of a pain to setup). In reality, there's so many ORMs and storage mechanisms available to Python that you should try as many as you can and use the one you like the most.

Lamson also supports extensive spam blocking through SpamBayes:

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue", next_state=SPAMMING)
def START(message, **kw):
    ....

Bounce detection and analysis is available that regular people can use without going RFC nuts:

@route("(anything)@(host)", anything=".+", host=".+")
@bounce_to(soft=SOFT_BOUNCED, hard=HARD_BOUNCED)
def START(message, **kw):
    ...

All within a framework that works well with existing legacy mail servers so you can gradually get into using Lamson.

The 30 Second Introduction

If you have Python and easy_install already, then try this out:
$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc
$ lamson stop -ALL run

You now have a working base Lamson setup ready for you to work on with everything you need installed.

Next Steps

Grab the code and you can read through the quick start documentation. After you've gone through that, best thing to do is read the code to the examples/osb example included in the source distribution and read the rest of the documentation on this site.

At any point, you can get help for all available Lamson commands with:

$ lamson help

You can get individual command help with:

$ lamson help -for start

Finally, if you really want to get started using Lamson to implement your dream email application, but are completely lost, then you can contact me for help.

lamson-1.0pre11/doc/lamsonproject.org/input/lists/0000755000076500000240000000000011313464573021563 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/lists/index.txt0000644000076500000240000000211111310754212023413 0ustar zedshawstaffFrom: Zed Title: Lamson Project Mailing Lists Lamson's mailing lists are hosted on "librelist.com":http://librelist.com/ which is also written in Lamson. You can subscribe to the list by sending an email to "lamson@librelist.com":mailto:lamson@librelist.com with your first message. *Remember that librelist posts this message, so introduce yourself.* If you want to read the archives for the Lamson list, you can browse them "here":http://librelist.com/browser/ in a very very basic form (soon to improve). You can also get the archives for the Lamson list by using rsync:
$ rsync -azv librelist.com::archives/lamson lamson_archives
Be careful when you do this since that will get everything. It's better to figure out which month or day you want using your web browser and then get that directory only. h2. IRC Channel For people who don't want to join the mailing list but still need help there's a #lamson irc channel on irc.freenode.org. Come by and ask your question, and stick around since it might take people a little while to respond. lamson-1.0pre11/doc/lamsonproject.org/input/releases/0000755000076500000240000000000011313464573022230 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/releases/index.txt0000644000076500000240000000172311313464030024067 0ustar zedshawstaffTitle: Lamson Releases You can get the Lamson source in various ways: * Download it from "support site 1.0pre11 tag":http://support.lamsonproject.org/vinfo/1.0pre11 as a "Zip file":http://support.lamsonproject.org/zip/Lamson%20Project-1.0pre11.zip?uuid=1.0pre11 * Use "Fossil":http://fossil-scm.org to get the code as described on the "support site":http://support.lamsonproject.org/home And don't forget you can install it with easy_install as well:
sudo easy_install --upgrade lamson
bq. If easy_install doesn't work, try without --upgrade, and if that doesn't work then look in your site-packages directory and delete all the versions of Lamson you find. h2. Releases You can get the 1.0pre11 release source from the this site as well: "1.0pre11 release tar.gz":/releases/lamson-1.0pre11.tar.gz "1.0pre11 release Python Egg":/releases/lamson-1.0pre11-py2.5.egg h2. Examples You can get all of the examples from the source distribution. lamson-1.0pre11/doc/lamsonproject.org/input/videos/0000755000076500000240000000000011313464573021716 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/input/videos/index.txt0000644000076500000240000000400011213304060023537 0ustar zedshawstaffTitle: Screencasts And Tutorials Content-Type: text/html

This is the first screencast that walks you through the first half of the Deploying Lamson tutorial. It is the first screencast so it's probably rough and the sound isn't so great. If you have tips and feedback on doing these better contact me and let me know.

WARNING: Seeking seems to not work with this player using the highquality video. Just let it stream or download it.

You can download this video from archive.org to get a better quality video.

lamson-1.0pre11/doc/lamsonproject.org/output/0000755000076500000240000000000011313464573020626 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/about.html0000644000076500000240000001375011230770143022623 0ustar zedshawstaff LamsonProject: About Lamson

About Lamson

Lamson started more than a year ago as a fun side project of mine after a few evil experiences with sendmail and various mailing list software. I realized that e-mail systems didn’t have anything like a modern “framework” for building applications. Rather than replicate the mess of aliases, pipes, processes, and nasty m4 macros that currently existed, I decided to write something different.

Lamson was originally called “Son Of Sam”, but that name proved too difficult to work with as a project name. Apparently people don’t like their software named after serial killers (actually, that was his dog’s name).

“Lamson Tubes” is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today.

Now Lamson is a fully functioning SMTP server, relay, proxy, and e-mail application framework. It supports most RDBMS, has templates, and is easy to manage and deploy. Lamson is smarter than most any e-mail processing system out there thanks to Python.

However, as great as Lamson is for processing email intelligently, it isn’t the best solution for delivering mail. There is 30+ years of SMTP lore and myth stored in the code of mail servers such as Postfix and Exim that would take years to replicate and make efficient. Being a practical project, Lamson defers to much more capable SMTP servers for the grunt work of getting the mail to the final recipient.

I currently use Lamson in my own work, but I’m always looking for people doing interesting things with e-mail. Even if you’re a spammer, or someone trying to destroy the spammers, I want to hear from you.

If you’ve got something interesting, feel free to contact me and talk about it.


lamson-1.0pre11/doc/lamsonproject.org/output/about.txt0000644000076500000240000000353711210043226022471 0ustar zedshawstaffFrom: Zed Title: About Lamson Lamson started more than a year ago as a fun side project of "mine":http://zedshaw.com/ after a few evil experiences with sendmail and various mailing list software. I realized that e-mail systems didn't have anything like a modern "framework" for building applications. Rather than replicate the mess of aliases, pipes, processes, and nasty m4 macros that currently existed, I decided to write something different. Lamson was originally called "Son Of Sam", but that name proved too difficult to work with as a project name. Apparently people don't like their software named after serial killers (actually, that was his dog's name). "Lamson Tubes" is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today. Now Lamson is a fully functioning SMTP server, relay, proxy, and e-mail application framework. It supports most RDBMS, has templates, and is easy to manage and deploy. Lamson is smarter than most any e-mail processing system out there thanks to Python. However, as great as Lamson is for processing email intelligently, it isn't the best solution for delivering mail. There is 30+ years of SMTP lore and myth stored in the code of mail servers such as "Postfix":http://www.postfix.org/ and "Exim":http://www.exim.org/ that would take years to replicate and make efficient. Being a practical project, Lamson defers to much more capable SMTP servers for the grunt work of getting the mail to the final recipient. I currently use Lamson in my own work, but I'm always looking for people doing interesting things with e-mail. Even if you're a spammer, or someone trying to destroy the spammers, I want to hear from you. If you've got something interesting, feel free to "contact me":/contact.html and talk about it. lamson-1.0pre11/doc/lamsonproject.org/output/blog/0000755000076500000240000000000011313464573021551 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-16.html0000644000076500000240000001145311230770143023472 0ustar zedshawstaff LamsonProject: Lamson Project Site Launched

Lamson Project Site Launched

Today I launched the Lamson Project site at lamsonproject.org and started filling in the content. Lamson is really turning into a fun and useful project, and hopefully the site will get other people interested in it and using it.

I took the design from one of the many free web design sites and reused the same Python blog script that I use on my own site so getting this up and running was cake.

Subscribe to the RSS feed and I’ll soon be doing a blog post announcing mailing lists and other email services that I’ll host on lamsonproject.org as well as new documentation and code drops.

If you browse around you’ll find some useful documentation, but nothing complete just yet. I’m still writing most of it.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-16.txt0000644000076500000240000000146011203540277023345 0ustar zedshawstafftitle: Lamson Project Site Launched Today I launched the Lamson Project site at "lamsonproject.org":http://lamsonproject.org/ and started filling in the content. Lamson is really turning into a fun and useful project, and hopefully the site will get other people interested in it and using it. I took the design from one of the many free web design sites and reused the same Python blog script that I use on "my own site":http://zedshaw.com/ so getting this up and running was cake. Subscribe to the "RSS feed":/feed.xml and I'll soon be doing a blog post announcing mailing lists and other email services that I'll host on lamsonproject.org as well as new documentation and code drops. If you browse around you'll find some useful documentation, but nothing complete just yet. I'm still writing most of it. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-18.html0000644000076500000240000002056311230770143023476 0ustar zedshawstaff LamsonProject: Bug Fix 0.8.4, Mailing Lists, Spam Blocking

Bug Fix 0.8.4, Mailing Lists, Spam Blocking

A few announcements from my work on Lamson the last few days. I managed to fix a bug, put Lamson to work doing Lamson’s mailing lists, and use Lamson to do some spam blocking on my own email account.

Hopefully eating my own dogfood won’t be too painful.

Grab 0.8.4, Important Bug Fix

I wasn’t using the SMTPServer class in Python correctly, and was stopping the whole server when a single channel had an error. It’s a one line fix and I’ve been running it for a few days with no problems.

Please make sure that you upgrade if you installed Lamson:

sudo easy_install —upgrade lamson

Or you can grab it from PyPI

Mailing Lists

Lamson is now running its very simple examples/mailinglist sample on lists.lamsonproject.org. You can send an email to:

lamson.users-subscribe@lists.lamsonproject.org

And try it out. The software running that is also what’s available in the examples/mailinglist sample (minus a few tweaks) so if you want to help make it a real functioning mailing list then feel free to contribute.

I’m subscribed there, and I’ll be working on it over the next week to turn it into a more complete and robust mailing list. An important thing I need to implement is bounce and vacation detection. I’ll probably also take my spam filter hacks and work them in as well.

Also, the software at lists.lamsonproject.org is an open mailing list system. Feel free to use it at your own risk, and if it becomes popular then I may turn it into a permanent thing.

As an open mailing list system, what it does is creates any list that doesn’t exist when you subscribe. Think of it as lazy loading for mailing lists. No need to fill out forms, beg for permission, alter aliases, edit text files, or run a ton of shell commands.

Just subscribe and go.

Now, if that turns into a massive abuse vector then I’ll turn it off, but I’d like to make it work for people since it demonstrates something simple that Lamson can do which other mailing list systems are bad at.

Spam Filtering And Graylists In The Works

My personal email account (zedshaw@zedshaw.com) is now running Spambayes inside of Lamson to filter spam. Here’s the code (stripped down for show):

class SpamHandler(server.MessageHandler):
    """Uses app.spam to mark all messages it receives as Spam."""
    def process(self, session, message, args):
        filter = spam.Filter()
        filter.train_spam(message.msg)
        filter.close()

class FilterHandler(server.MessageHandler):
    """
    Uses the spam filter to either queue up a message
    into the run/queue for later review, or forward 
    it on if it is not spam.
    """
    def process(self, session, message, args):
        classifier = spam.Filter( )
        classifier.filter(message.msg)
        if message.msg['X-Spambayes-Classification'].startswith('spam'):
            q = queue.Queue('run/spam')
            q.push(str(message))
        else:
            self.relay.deliver(message)

This shows Lamson’s raw “handler style” of extension, which the Finite State Machine stuff is built on. These are wired into the config/settings.py file routing so that any mail that’s sent to zedshaw@zedshaw.com goes through FilterHandler and any mail that sent to bogus addresses @zedshaw.com goes through SpamHandler.

It turns out that a lot of spam is delivered to random addresses at zedshaw.com, so this works as an autotraining mechanism. Well, at least until some asshat figures out he can game my spam filter.

The FilterHandler then takes messages that SpamBayes knows is spam and puts it into a queue for later review. I periodically check that folder and pull out any false positives for retraining.

It’s all very hacky right now, but seems to be working really well, so once I work out the full workflow I’ll formalize this code and add it to Lamson as extra handlers to use. I’ll also implement a simple graylisting system and include that too.

Well, enjoy the work so far and shoot me any comments you have.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-18.txt0000644000076500000240000001014411213057130023336 0ustar zedshawstaffTitle: Bug Fix 0.8.4, Mailing Lists, Spam Blocking A few announcements from my work on Lamson the last few days. I managed to fix a bug, put Lamson to work doing Lamson's mailing lists, and use Lamson to do some spam blocking on my own email account. Hopefully eating my own dogfood won't be too painful. h2. Grab 0.8.4, Important Bug Fix I wasn't using the SMTPServer class in Python correctly, and was stopping the whole server when a single channel had an error. It's a one line fix and I've been running it for a few days with no problems. Please make sure that you upgrade if you installed Lamson: sudo easy_install --upgrade lamson Or you can "grab it from PyPI":http://pypi.python.org/pypi?name=lamson&version=0.8.4&:action=display h2. Mailing Lists Lamson is now running its very simple _examples/mailinglist_ sample on lists.lamsonproject.org. You can send an email to: lamson.users-subscribe@lists.lamsonproject.org And try it out. The software running that is also what's available in the _examples/mailinglist_ sample (minus a few tweaks) so if you want to help make it a real functioning mailing list then feel free to contribute. I'm subscribed there, and I'll be working on it over the next week to turn it into a more complete and robust mailing list. An important thing I need to implement is bounce and vacation detection. I'll probably also take my spam filter hacks and work them in as well. Also, the software at lists.lamsonproject.org is an open mailing list system. Feel free to use it at your own risk, and if it becomes popular then I may turn it into a permanent thing. As an open mailing list system, what it does is creates any list that doesn't exist when you subscribe. Think of it as lazy loading for mailing lists. No need to fill out forms, beg for permission, alter aliases, edit text files, or run a ton of shell commands. Just subscribe and go. Now, if that turns into a massive abuse vector then I'll turn it off, but I'd like to make it work for people since it demonstrates something simple that Lamson can do which other mailing list systems are bad at. h2. Spam Filtering And Graylists In The Works My personal email account (zedshaw@zedshaw.com) is now running "Spambayes":http://spambayes.sourceforge.net/ inside of Lamson to filter spam. Here's the code (stripped down for show):
class SpamHandler(server.MessageHandler):
    """Uses app.spam to mark all messages it receives as Spam."""
    def process(self, session, message, args):
        filter = spam.Filter()
        filter.train_spam(message.msg)
        filter.close()

class FilterHandler(server.MessageHandler):
    """
    Uses the spam filter to either queue up a message
    into the run/queue for later review, or forward 
    it on if it is not spam.
    """
    def process(self, session, message, args):
        classifier = spam.Filter( )
        classifier.filter(message.msg)
        if message.msg['X-Spambayes-Classification'].startswith('spam'):
            q = queue.Queue('run/spam')
            q.push(str(message))
        else:
            self.relay.deliver(message)
This shows Lamson's raw "handler style" of extension, which the Finite State Machine stuff is built on. These are wired into the config/settings.py file routing so that any mail that's sent to zedshaw@zedshaw.com goes through *FilterHandler* and any mail that sent to bogus addresses @zedshaw.com goes through *SpamHandler*. It turns out that a lot of spam is delivered to random addresses at zedshaw.com, so this works as an autotraining mechanism. Well, at least until some asshat figures out he can game my spam filter. The FilterHandler then takes messages that SpamBayes knows is spam and puts it into a queue for later review. I periodically check that folder and pull out any false positives for retraining. It's all very hacky right now, but seems to be working really well, so once I work out the full workflow I'll formalize this code and add it to Lamson as extra handlers to use. I'll also implement a simple graylisting system and include that too. Well, enjoy the work so far and shoot me any comments you have. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-19.html0000644000076500000240000001305611230770143023476 0ustar zedshawstaff LamsonProject: New Site Look, Same Great Content

New Site Look, Same Great Content

This is just a quick update to say thanks to Ken Keiter for creating a new lamsonproject.org site layout and design. The new site should be easier to read, have more breathing room, and look easier on the eyes. It’s even got a logo:

Which I like quite a lot.

Feel free to shoot your comments about the design to me.

Two Days Of Spam Filtering

So far the new Lamson based spam blocking and filtering is working pretty well. The majority of spam is blocked, however the success rate of SpamBayes needs to improve. Right now I’ve trained it with a large set of ham, and with a reasonable number of spam messages as I encounter them. I think it’s received about a 20/1 (ham/spam) ratio for training.

With that training it’s sorted about 50 spam, no false positives, and missed about 15 spam as “unsure”, with about 4 as “ham”. The 4 that were classified as ham were just one liners with a link in them. The unsure spam probably could have just been treated as spam, but I’m being conservative while I test it out.

Hopefully the success rate improves over the next few days. SpamBayes is great for being a Python library I can use easily, but if it’s success rate doesn’t get much higher then I may look at other options. Of course, there are more important things I could be doing right now too.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-19.txt0000644000076500000240000000274111204561532023351 0ustar zedshawstaffTitle: New Site Look, Same Great Content This is just a quick update to say thanks to "Ken Keiter":http://kenkeiter.com/ for creating a new "lamsonproject.org":http://lamsonproject.org/ site layout and design. The new site should be easier to read, have more breathing room, and look easier on the eyes. It's even got a logo: !http://lamsonproject.org/images/lamson.png! Which I like quite a lot. Feel free to shoot your comments about the design to "me.":/contact.html h2. Two Days Of Spam Filtering So far the new Lamson based spam blocking and filtering is working pretty well. The majority of spam is blocked, however the success rate of "SpamBayes":http://spambayes.sourceforge.net/ needs to improve. Right now I've trained it with a large set of ham, and with a reasonable number of spam messages as I encounter them. I think it's received about a 20/1 (ham/spam) ratio for training. With that training it's sorted about 50 spam, no false positives, and missed about 15 spam as "unsure", with about 4 as "ham". The 4 that were classified as ham were just one liners with a link in them. The unsure spam probably could have just been treated as spam, but I'm being conservative while I test it out. Hopefully the success rate improves over the next few days. SpamBayes is great for being a Python library I can use easily, but if it's success rate doesn't get much higher then I may look at other options. Of course, there are more important things I could be doing right now too. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-20.html0000644000076500000240000001024111230770143023457 0ustar zedshawstaff LamsonProject: Lamson Project Ideas

Lamson Project Ideas

I wrote a blog post about project ideas for Lamson on my personal blog. Head on over if you’re looking for something to hack on, or just want something to read that isn’t about the web.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-20.txt0000644000076500000240000000040411205152004023322 0ustar zedshawstaffTitle: Lamson Project Ideas I wrote a "blog post about project ideas for Lamson":http://zedshaw.com/blog/2009-05-20.html on my personal blog. Head on over if you're looking for something to hack on, or just want something to read that isn't about the web. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-24.html0000644000076500000240000002210711230770143023467 0ustar zedshawstaff LamsonProject: Features For The 0.9 Release (Soon)

Features For The 0.9 Release (Soon)

I've been hard at work cooking up the very nice new routing system, and I must say it is rather tasty. I've gone and created a whole new routing and state management design that uses decorators right in your handler modules to indicate how each state will expect mail addresses.

For the 0.9 release happening tomorrow I've got a new Router setup, spam filtering baked in nice and clean, improved test coverage, and indirect state storage for those who don't like SQLAlchemy.

New Routing Decorators

The new routing design is very nice if I do say so myself. It should reduce the amount of Python regex wizardry you need to learn, help set reasonable defaults, and put your handlers and routing in one spot so they are easier to maintain. In addition the new design eliminated many files and flaws from the previous design.

Here's a quick sample from the unit tests:

from lamson.routing import Router, route, route_like

Router.defaults(host="test.com", 
                action="[a-zA-Z0-9]+",
                list_name="[a-zA-Z.0-9]+")


@route("(list_name)-(action)@(host)")
def START(message, list_name=None, action=None, host=None):
    print "START", message, list_name, action, host
    if action == 'explode':
        print "EXPLODE!"
        raise RuntimeError("Exploded on purpose.")
    return UNKNOWN
    

@route("(list_name)-(action)@(host)")
def UNKNOWN(message, list_name=None, action=None, host=None):
    print "UNKNOWN", message, list_name, action, host
    return NEXT


@route_like(UNKNOWN)
def NEXT(message, list_name=None, action=None, host=None):
    print "NEXT", message, list_name, action, host
    return CONFIRM

@route_like(UNKNOWN)
def CONFIRM(message, list_name=None, action=None, host=None):
    print "CONFIRM", message, list_name, action, host
    return END

@route("(anything)@(host)", anything=".*")
def END(message, anything=None, host=None):
    print "END", anything, host
    return START

The formatting on the @route decorator will hopefully simplify the use of regular expressions. Rather the above route for "(list_name)-(action)@(host)" gets translated by Lamson into '^(?P[a-zA-Z.0-9]+)-(?P[a-zA-Z0-9]+)@(?Ptest.com)$' which is a hair pulling monstrosity. It also is using defaults so that you only have to indicate basic formatting of the email and leave the regexes to the main configuration file.

This new setup also has some interesting implications in how you can use it. For example, you can register multiple address forms for each state (UNKNOWN, END, etc.) so that it can handle similar activity.

Spam Filtering Per State

Now that the routing is nice and generic, I could implement a decorator for using SpamBayes to filter spam to your state functions. If you don't want a particular state to receive spam then you just use the @spam_filter decorator:

from lamson.routing import route, route_like
from lamson.spam import spam_filter

ham_db = "tests/sddb"

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue")
def START(message, **kw):
    print "Ham message received. Going to END."
    return END

@route_like(START)
def END(message, *kw):
    print "Done."

This simply attaches the @spam_filter decorator to START and then when START runs it transitions to END. It's still a little rough since you're having to configure the @spam_filter right there, rather than in a config/settings.py file, but it works. Later versions will be much cleaner.

Indirect State Storage

I've also abstracted away the state storage, which will open the door to backends in any kind of storage you want, not just SQLAlchemy. The only thing you'll need to implement to use a different storage is a simple get/set/clear set of functionality and then attach it to the Router class to make it use the new storage.

Currently the code is using a simple MemoryStorage class to speed up the unit test runs, and then I'll implement the SQLAlchemy store and probably a Tokyo Tyrant store too.

Higher Test Coverage

I also worked on getting the unit test coverage up as high as I could. Since Lamson is a server with nasty OS level things like sockets and daemons, it is difficult to fully test in just simple unit tests. It's at about 85% right now with only the daemon and server commands not being tested.

Finally, this new design gets rid of the old class based handlers, the Conversation style of Finite State Machine handlers, and quite a bit of code that was design cruft from my previous iterations. It should be much cleaner and meaner, and will have a nicer experience for developers.

Hold On Though

The code in the bzr branch is still in a state of flux, so feel free to grab it and look, but you probably don't want to actually do anything with it. I should have all the samples converted to the new style and then I'll do a 0.9 release. It should happen tomorrow (May 25th).

I'll have another blog post and some documentation for the new stuff tomorrow and all next week.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-24.txt0000644000076500000240000001232211213057130023333 0ustar zedshawstaffTitle: Features For The 0.9 Release (Soon) Content-Type: text/html

I've been hard at work cooking up the very nice new routing system, and I must say it is rather tasty. I've gone and created a whole new routing and state management design that uses decorators right in your handler modules to indicate how each state will expect mail addresses.

For the 0.9 release happening tomorrow I've got a new Router setup, spam filtering baked in nice and clean, improved test coverage, and indirect state storage for those who don't like SQLAlchemy.

New Routing Decorators

The new routing design is very nice if I do say so myself. It should reduce the amount of Python regex wizardry you need to learn, help set reasonable defaults, and put your handlers and routing in one spot so they are easier to maintain. In addition the new design eliminated many files and flaws from the previous design.

Here's a quick sample from the unit tests:

from lamson.routing import Router, route, route_like

Router.defaults(host="test.com", 
                action="[a-zA-Z0-9]+",
                list_name="[a-zA-Z.0-9]+")


@route("(list_name)-(action)@(host)")
def START(message, list_name=None, action=None, host=None):
    print "START", message, list_name, action, host
    if action == 'explode':
        print "EXPLODE!"
        raise RuntimeError("Exploded on purpose.")
    return UNKNOWN
    

@route("(list_name)-(action)@(host)")
def UNKNOWN(message, list_name=None, action=None, host=None):
    print "UNKNOWN", message, list_name, action, host
    return NEXT


@route_like(UNKNOWN)
def NEXT(message, list_name=None, action=None, host=None):
    print "NEXT", message, list_name, action, host
    return CONFIRM

@route_like(UNKNOWN)
def CONFIRM(message, list_name=None, action=None, host=None):
    print "CONFIRM", message, list_name, action, host
    return END

@route("(anything)@(host)", anything=".*")
def END(message, anything=None, host=None):
    print "END", anything, host
    return START

The formatting on the @route decorator will hopefully simplify the use of regular expressions. Rather the above route for "(list_name)-(action)@(host)" gets translated by Lamson into '^(?P[a-zA-Z.0-9]+)-(?P[a-zA-Z0-9]+)@(?Ptest.com)$' which is a hair pulling monstrosity. It also is using defaults so that you only have to indicate basic formatting of the email and leave the regexes to the main configuration file.

This new setup also has some interesting implications in how you can use it. For example, you can register multiple address forms for each state (UNKNOWN, END, etc.) so that it can handle similar activity.

Spam Filtering Per State

Now that the routing is nice and generic, I could implement a decorator for using SpamBayes to filter spam to your state functions. If you don't want a particular state to receive spam then you just use the @spam_filter decorator:

from lamson.routing import route, route_like
from lamson.spam import spam_filter

ham_db = "tests/sddb"

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue")
def START(message, **kw):
    print "Ham message received. Going to END."
    return END

@route_like(START)
def END(message, *kw):
    print "Done."

This simply attaches the @spam_filter decorator to START and then when START runs it transitions to END. It's still a little rough since you're having to configure the @spam_filter right there, rather than in a config/settings.py file, but it works. Later versions will be much cleaner.

Indirect State Storage

I've also abstracted away the state storage, which will open the door to backends in any kind of storage you want, not just SQLAlchemy. The only thing you'll need to implement to use a different storage is a simple get/set/clear set of functionality and then attach it to the Router class to make it use the new storage.

Currently the code is using a simple MemoryStorage class to speed up the unit test runs, and then I'll implement the SQLAlchemy store and probably a Tokyo Tyrant store too.

Higher Test Coverage

I also worked on getting the unit test coverage up as high as I could. Since Lamson is a server with nasty OS level things like sockets and daemons, it is difficult to fully test in just simple unit tests. It's at about 85% right now with only the daemon and server commands not being tested.

Finally, this new design gets rid of the old class based handlers, the Conversation style of Finite State Machine handlers, and quite a bit of code that was design cruft from my previous iterations. It should be much cleaner and meaner, and will have a nicer experience for developers.

Hold On Though

The code in the bzr branch is still in a state of flux, so feel free to grab it and look, but you probably don't want to actually do anything with it. I should have all the samples converted to the new style and then I'll do a 0.9 release. It should happen tomorrow (May 25th).

I'll have another blog post and some documentation for the new stuff tomorrow and all next week.

lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-28.html0000644000076500000240000001634111230770143023476 0ustar zedshawstaff LamsonProject: 0.9-pre2 Up For Testing, Docs Too

0.9-pre2 Up For Testing, Docs Too

First off, my apologies to everyone if your RSS reader went crazy today. I include documentation changes in the RSS feed so that people can easily track updates to the Lamson docs. However, that means when I’m writing a lot of documentation it hits the feed repeatedly.

The good news though is that you can now get at the 0.9-pre2 release of Lamson from the releases page and you can read the great new documentation I’ve got.

Simply go to the documentation list and go through. You’ll want to check out how to install Lamson in a virtualenv by Zachary Voase. You’ll also want to read through the current draft of the getting started as it covers all the nitty gritty things you need.

Migrating To 0.9

I’m getting the 0.9 pre-releases out so that people can start migrating and feeding me problems they encounter.

I’ll have a full list of changes and some documentation on migrating to 0.9. The main things that changed are covered in the getting started documentation, notably how Lamson is configured and how routing works. Routing is the biggest change of all so read through it.

I’ll have documentation on exactly how Routing works and how to use it too coming soon.

If you started using Lamson at the 0.8.x level and need help migrating contact me and I’ll help you out. Migration should involve just tagging your state functions with the right route decorators.

Mailing List Is Dead, Long Live OSB

I really didn’t like the way I did the mailinglist example, so rather than keep it alive I just killed it and will rewrite it after 0.9. In its place is the examples/osb application which is a One-Shot-Blog. You send it an email and it makes a blog page and index for it. It’s simple enough for everyone to comprehend and I’ll be able to cover it in documentation easily.

The OSB sample has all the right stuff for you to see how all of Lamson’s features work, so check it out and use it as the basis for your own applications.

You can get it by downloading the source and looking in the examples/osb directory.

0.9 Probably This Weekend

I want to get all the documentation done for the 0.9 release and finish the very last feature that it needs. I’m hoping to have that in the next few days, at least by Monday.

The only feature remaining for 0.9 is a SQLAlchemy based state storage, and maybe a shelf based one. Lamson now stores its state in an abstract StateStorage class that lets you store state however you need.

Right now all that’s implemented is a MemoryStorage which actually works well enough for most applications that you just start off, and is great for unit testing. But, there needs to be a permanent state store support.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-28.txt0000644000076500000240000000564311210151522023344 0ustar zedshawstaffTitle: 0.9-pre2 Up For Testing, Docs Too First off, my apologies to everyone if your RSS reader went crazy today. I include documentation changes in the RSS feed so that people can easily track updates to the Lamson docs. However, that means when I'm writing a lot of documentation it hits the feed repeatedly. The good news though is that you can now get at the 0.9-pre2 release of Lamson from the "releases":/releases/ page and you can read the great new documentation I've got. Simply go to the "documentation":/docs/ list and go through. You'll want to check out "how to install Lamson in a virtualenv":/docs/lamson_virtual_env.html by Zachary Voase. You'll also want to read through the current draft of the "getting started":/docs/getting_started.html as it covers all the nitty gritty things you need. h2. Migrating To 0.9 I'm getting the 0.9 pre-releases out so that people can start migrating and feeding me problems they encounter. I'll have a full list of changes and some documentation on migrating to 0.9. The main things that changed are covered in the "getting started":/docs/getting_started.html documentation, notably how Lamson is configured and how routing works. Routing is the biggest change of all so read through it. I'll have documentation on exactly how Routing works and how to use it too coming soon. If you started using Lamson at the 0.8.x level and need help migrating "contact me":/contact.html and I'll help you out. Migration should involve just tagging your state functions with the right route decorators. h2. Mailing List Is Dead, Long Live OSB I really didn't like the way I did the mailinglist example, so rather than keep it alive I just killed it and will rewrite it after 0.9. In its place is the _examples/osb_ application which is a One-Shot-Blog. You send it an email and it makes a blog page and index for it. It's simple enough for everyone to comprehend and I'll be able to cover it in documentation easily. The OSB sample has all the right stuff for you to see how all of Lamson's features work, so check it out and use it as the basis for your own applications. You can get it by "downloading the source":/releases/ and looking in the examples/osb directory. h2. 0.9 Probably This Weekend I want to get all the documentation done for the 0.9 release and finish the *very* last feature that it needs. I'm hoping to have that in the next few days, at least by Monday. The only feature remaining for 0.9 is a SQLAlchemy based state storage, and maybe a shelf based one. Lamson now stores its state in an abstract "StateStorage":/docs/api/lamson.routing.StateStorage-class.html class that lets you store state however you need. Right now all that's implemented is a "MemoryStorage":/docs/api/lamson.routing.MemoryStorage-class.html which actually works well enough for most applications that you just start off, and is great for unit testing. But, there needs to be a permanent state store support. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-31.html0000644000076500000240000001205411230770143023465 0ustar zedshawstaff LamsonProject: Lamson 0.9 Later Today

Lamson 0.9 Later Today

I have been working hard on the documentation and scrubbing the code for Lamson and the 0.9 release coming out soon. The only things I feel I need to do before an official 0.9 release are:

  1. Clean up the one-shot-blog demo application a bit more.
  2. Write a small set of instructions on writing your own StateStorage.
  3. Do a last final code review to check for any obvious problems.

Once I do that I’ll make 0.9 official. If you’re using 0.8.x right now, or just playing with Lamson, please go grab the 0.9 pre-release and make sure it works.

There is one bug in that 0.9-pre3 release where the router won’t properly route of the form:

To: "First Last" 

This is fairly rare, since someone would have to craft an email like this, and it would just be ignored anyway. That bug is fixed in the current 0.9 source.

Please shoot me any bugs you find and I’ll work them into the 0.9 release.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-05-31.txt0000644000076500000240000000203711210533442023335 0ustar zedshawstaffTitle: Lamson 0.9 Later Today I have been working hard on the documentation and scrubbing the code for Lamson and the 0.9 release coming out soon. The only things I feel I need to do before an official 0.9 release are: # Clean up the one-shot-blog demo application a bit more. # Write a small set of instructions on writing your own "StateStorage.":http://lamsonproject.org/docs/api/lamson.routing.StateStorage-class.html # Do a last final code review to check for any obvious problems. Once I do that I'll make 0.9 official. If you're using 0.8.x right now, or just playing with Lamson, please go grab the "0.9 pre-release":/releases/ and make sure it works. There is one bug in that 0.9-pre3 release where the router won't properly route of the form:
To: "First Last" 
This is fairly rare, since someone would have to craft an email like this, and it would just be ignored anyway. *That bug is fixed in the current 0.9 source.* Please shoot me any bugs you find and I'll work them into the 0.9 release. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-01.html0000644000076500000240000002003011230770143023454 0ustar zedshawstaff LamsonProject: Lamson 0.9 Is Out, Find My Bugs!

Lamson 0.9 Is Out, Find My Bugs!

I just pushed Lamson 0.9 up to PyPI for everyone to grab and break. This release features a complete redesign of the routing, state handling, templating, and a full set of very complete documentation. Everyone who was using 0.8.x series should be able to migrate to this version with some work, but it won’t be terribly painful (assuming you have unit tests).

Getting 0.9

Easiest way to install is with easy_install straight from PyPI:

sudo easy_install lamson

There’s also instructions on installing to a virtualenv for those who like that.

Best thing to do is read the Getting Started instructions and when you’re done with at least the 30 second introduction you’ll have your Lamson.

Try to go through the whole document if you can and send me feedback.

Big Changes

The biggest change is that the routing mechanism and internals are totally different. Now instead of putting all your routes into the config/settings.py as a big array, you just put them on each state function as a decorator. To help you debug these fancy routes there’s also a new lamson routes command that will dump them in their final state.

The biggest impact of the new design is the old school “BlahHandler” classes are gone. Later versions of Lamson might bring back the ability to use a class as a handler.

After that comes the removal of depending on SQLAlchemy for state storage. You can now write your own state storage and store things however you need to whatever you need. There is currently a simple storage for storing in memory and for using a Python shelve dict.

Next there was a change to how Lamson boots up and configures itself giving you more control over what it uses, and giving your handlers better access to your gear. This change also let’s you use better unit tests that don’t require a running lamson server, as well as giving you the old “integration test” style tests that hit your real server.

There’s also tentative support for PyEnchant spell checking of your templates, and a new lamson spell command to help you use it. See the getting started document for how to configure that, but be warned that PyEnchant is kind of a mean install.

Finally, I switched to Jinja2 templates by default, but you can use Mako fairly easily by altering a configuration variable in the boot.py.

Look At All The Shiny Docs

In addition to redesigning the guts of Lamson so that it’s easier to work on and use, I also spent a lot of time cranking out good documentation so people can get started using it. Take a look in the documentation section and have a gander at the API documentation that is generated from the source documentation.

New Example: One-Shot-Blog

In the source distribution you can grab here there’s an example of doing a simple blog that uses just email to manage the blogger’s posts. The example replaces the mailing list example (for now) as it is simpler and doesn’t use a database.

Please Break It

The 0.9 release is the result of a lot of work, but I want people to break it and criticize it to death. The goal is to have people beat up this basic functionality, producing a series of 0.9.x releases, and then when everything is tight doing a 1.0 release.

Thanks Everyone

I’d like to thank everyone on Twitter, Hacker News, and yes, even Reddit for their comments both good and bad. It helped me make good decisions knowing what people wanted, so keep the feedback coming.

Thanks also to those who donated documentation, praise, or other help directly to the project.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-01.txt0000644000076500000240000000737511210662772023356 0ustar zedshawstaffTitle: Lamson 0.9 Is Out, Find My Bugs! I just pushed Lamson 0.9 up to PyPI for everyone to grab and break. This release features a complete redesign of the routing, state handling, templating, and a full set of very complete documentation. Everyone who was using 0.8.x series should be able to migrate to this version with some work, but it won't be terribly painful (assuming you have unit tests). h2. Getting 0.9 Easiest way to install is with "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall straight from PyPI:
sudo easy_install lamson
There's also instructions on "installing to a virtualenv":/docs/lamson_virtual_env.html for those who like that. Best thing to do is read the "Getting Started":/docs/getting_started.html instructions and when you're done with at least the 30 second introduction you'll have your Lamson. Try to go through the whole document if you can and send me feedback. h2. Big Changes The biggest change is that the routing mechanism and internals are totally different. Now instead of putting all your routes into the config/settings.py as a big array, you just put them on each state function as a decorator. To help you debug these fancy routes there's also a new @lamson routes@ command that will dump them in their final state. The biggest impact of the new design is the old school "BlahHandler" classes are gone. Later versions of Lamson might bring back the ability to use a class as a handler. After that comes the removal of depending on SQLAlchemy for state storage. You can now "write your own state storage":/docs/writing_a_state_storage.html and store things however you need to whatever you need. There is currently a simple storage for storing in memory and for using a Python shelve dict. Next there was a change to how Lamson boots up and configures itself giving you more control over what it uses, and giving your handlers better access to your gear. This change also let's you use better unit tests that don't require a running lamson server, as well as giving you the old "integration test" style tests that hit your real server. There's also tentative support for PyEnchant spell checking of your templates, and a new @lamson spell@ command to help you use it. See the "getting started":/docs/getting_started.html document for how to configure that, but be warned that PyEnchant is kind of a mean install. Finally, I switched to Jinja2 templates by default, but you can use Mako fairly easily by altering a configuration variable in the boot.py. h2. Look At All The Shiny Docs In addition to redesigning the guts of Lamson so that it's easier to work on and use, I also spent a lot of time cranking out good documentation so people can get started using it. Take a look in the "documentation":/docs section and have a gander at the "API documentation":/docs/api/ that is generated from the source documentation. h2. New Example: One-Shot-Blog In the source distribution "you can grab here":/releases/ there's an example of doing a simple blog that uses just email to manage the blogger's posts. The example replaces the mailing list example (for now) as it is simpler and doesn't use a database. h2. Please Break It The 0.9 release is the result of a lot of work, but I want people to break it and criticize it to death. The goal is to have people beat up this basic functionality, producing a series of 0.9.x releases, and then when everything is tight doing a 1.0 release. h2. Thanks Everyone I'd like to thank everyone on Twitter, Hacker News, and yes, even Reddit for their comments both good and bad. It helped me make good decisions knowing what people wanted, so keep the feedback coming. Thanks also to those who donated documentation, praise, or other help directly to the project. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-03-2.html0000644000076500000240000001520611230770143023626 0ustar zedshawstaff LamsonProject: Lamson 0.9.2, Test Coverage 97%

Lamson 0.9.2, Test Coverage 97%

The 0.9.2 release is out and ready for everyone to easy_install. I spent the day getting rid of my tech debt by boosting the Lamson test coverage to a whopping 97%.

I also wrote new documentation on Running A Queue Receiver in a separate process using Lamson.

Test Coverage 97%

I used the very awesome mock library which let me mock out a bunch of the system calls that the lamson.commands module accesses to do its work.

Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               120    120   100%   
lamson.commands           185    178    96%   199-200, 347-349, 367, 369
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.mail               122    121    99%   234
lamson.queue               30     30   100%   
lamson.routing            185    184    99%   417
lamson.server              85     84    98%   182
lamson.spam                80     74    92%   38-40, 59, 65-66
lamson.utils               53     50    94%   57, 70-71
lamson.view                15     15   100%   
-----------------------------------------------------
TOTAL                     885    866    97%   
----------------------------------------------------------------------
Ran 84 tests in 7.056s

By mocking out things like sys.exit and sockets I’m able to run the code and make sure those methods were called correctly. I could also throw exceptions to make sure that exception handlers ran correctly.

Two Bugs Fixed

Interestingly enough, increasing the test coverage to 97% didn’t find any more bugs. I did find one small bug and one obvious bug in the 0.9.1 release.

  1. If you gave a trailing option that wasn’t a string it would throw an exception.
  2. If you tried to start with a different boot script then it wouldn’t do it.

These are now fixed in the 0.9.2 release, so make sure you grab it with the easiest method being:

$ sudo easy_install lamson

IRC Channel on irc.freenode.org

I’m now hanging out in the #lamson channel on irc.freenode.org. Anyone who wants to come and chat about lamson can stop by. I should be there all the time, but I may not respond immediately.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-03-2.txt0000644000076500000240000000504411211630350023472 0ustar zedshawstaffTitle: Lamson 0.9.2, Test Coverage 97% The 0.9.2 release is out and ready for everyone to easy_install. I spent the day getting rid of my tech debt by boosting the Lamson test coverage to a whopping 97%. I also wrote new documentation on "Running A Queue Receiver":http://lamsonproject.org/docs/deferred_processing_to_queues.html in a separate process using Lamson. h2. Test Coverage 97% I used the very awesome "mock":http://www.voidspace.org.uk/python/mock/ library which let me mock out a bunch of the system calls that the "lamson.commands":http://lamsonproject.org/docs/api/lamson.commands-module.html module accesses to do its work.
Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               120    120   100%   
lamson.commands           185    178    96%   199-200, 347-349, 367, 369
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.mail               122    121    99%   234
lamson.queue               30     30   100%   
lamson.routing            185    184    99%   417
lamson.server              85     84    98%   182
lamson.spam                80     74    92%   38-40, 59, 65-66
lamson.utils               53     50    94%   57, 70-71
lamson.view                15     15   100%   
-----------------------------------------------------
TOTAL                     885    866    97%   
----------------------------------------------------------------------
Ran 84 tests in 7.056s
By mocking out things like @sys.exit@ and sockets I'm able to run the code and make sure those methods were called correctly. I could also throw exceptions to make sure that exception handlers ran correctly. h2. Two Bugs Fixed Interestingly enough, increasing the test coverage to 97% didn't find any more bugs. I did find one small bug and one obvious bug in the 0.9.1 release. # If you gave a trailing option that wasn't a string it would throw an exception. # If you tried to start with a different boot script then it wouldn't do it. These are now fixed in the 0.9.2 release, so make sure you "grab it":/download.html with the easiest method being:
$ sudo easy_install lamson
h2. IRC Channel on irc.freenode.org I'm now hanging out in the #lamson channel on irc.freenode.org. Anyone who wants to come and chat about lamson can stop by. I should be there all the time, but I may not respond immediately. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-03.html0000644000076500000240000001406211230770144023467 0ustar zedshawstaff LamsonProject: Lamson 0.9.1 Out, New Docs

Lamson 0.9.1 Out, New Docs

I released Lamson 0.9.1 today so please grab it and test it and shoot me feedback.

The little changes are easier handling of attachments in MailRequest, a small tweak to spam_filter, and then a bunch of test enhancements and extra gear. The big change is a redesign to lamson.args so that it infers the command line arguments from the lamson.commands function keyword arguments. It’s mostly an internal change since others can’t write their own commands (yet).

New Documentation

I ran into a bunch of folks who didn’t know that Lamson had built-in spam filtering, so I wrote a document on Filtering Spam With Lamson that tells you how to use it.

Lamson’s spam filtering support is fairly simplistic, but still useful for filtering crap out of your handlers. It’s biggest problem is the current implementation requires you to use the (barely documented) “SpamBayes” command line tools for things like training.

I’ve got some instructions to get you started with the most important SpamBayes command line tool you’ll need for training.

ChangeLog

Here’s the log of what I did:

  • Big code review, cleanup, and test boosting.
  • Modified the MailRequest.body so that it works with attachments.
  • Updated the OSB in preparation for a redesign to make it more interesting.
  • Didn’t like the name 'attachments’ for getting what’s really all the parts of a message.
  • Implemented a method on MailRequest to return all of the attachments in a message reliably.
  • Setup the muttrc to use env to find the lamson command.
  • Updated the commands and utils so that it works with the new lamson.args setup.
  • Refactored lamson.args to use inspect to figure out the required defaults of a command function, thus removing the need for a…
  • Contributed change to muttrc so it uses lamson wherever it is installed, even virtualenv.

lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-03.txt0000644000076500000240000000362311211415751023342 0ustar zedshawstaffTitle: Lamson 0.9.1 Out, New Docs I released Lamson 0.9.1 today so please "grab it and test it":/download.html and shoot me feedback. The little changes are easier handling of attachments in MailRequest, a small tweak to spam_filter, and then a bunch of test enhancements and extra gear. The big change is a redesign to lamson.args so that it infers the command line arguments from the lamson.commands function keyword arguments. It's mostly an internal change since others can't write their own commands (yet). h2. New Documentation I ran into a bunch of folks who didn't know that Lamson had built-in spam filtering, so I wrote a document on "Filtering Spam With Lamson":http://lamsonproject.org/docs/filtering_spam.html that tells you how to use it. Lamson's spam filtering support is fairly simplistic, but still useful for filtering crap out of your handlers. It's biggest problem is the current implementation requires you to use the (barely documented) "SpamBayes" command line tools for things like training. I've got some instructions to get you started with the most important SpamBayes command line tool you'll need for training. h2. ChangeLog Here's the log of what I did: * Big code review, cleanup, and test boosting. * Modified the MailRequest.body so that it works with attachments. * Updated the OSB in preparation for a redesign to make it more interesting. * Didn't like the name 'attachments' for getting what's really all the parts of a message. * Implemented a method on MailRequest to return all of the attachments in a message reliably. * Setup the muttrc to use env to find the lamson command. * Updated the commands and utils so that it works with the new lamson.args setup. * Refactored lamson.args to use inspect to figure out the required defaults of a command function, thus removing the need for a... * Contributed change to muttrc so it uses lamson wherever it is installed, even virtualenv. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-04.html0000644000076500000240000001543111230770144023471 0ustar zedshawstaff LamsonProject: OneShotBlog Sample (Hack) Running

OneShotBlog Sample (Hack) Running

I finally got off my ass and put the OneShotBlog sample up. This is the code (plus a few little tweaks) from the sample that is in the Lamson source running on another server. It’s using all the features of Lamson, include the new Queue Receiver functionality. So far it’s working great considering I’ve just been hacking on it on the side to try out the usability of the Lamson APIs and not really taken it seriously.

The concept of the oneshotblog is a little weird:

  1. You send an email to SOMETAG@oneshotblog.com, it can have textile markup.
    1. SOMETAG can be anything that’s a tag (chars, ints, periods, no leading periods), like: i.want.my.blog@oneshotblog.com
  2. That is posted for you at http://oneshotblog.com/posts/YOURADDRESS/SOMETAG.html
    1. I do block attempts to use a bad path by whitelisting. If you send something messed up for the tag it’s ignored.
  3. Then your post is summarized and tossed on the front page with the most recent 50 shown.
    1. This is done by a deferred queue receiver that indexes messages as it receives them.
  4. If you send to that same tag, it replaces it with the new contents (although the index will show the old stuff for a while).

There’s some dumb bugs in it currently, like delete doesn’t remove your message from the index and other fun things, but so far it’s running as expected given the work I put into it.

Anyway, feel free to try it out and shoot me feedback. Try sending really broken email too so that I can see what Lamson does with it.

Future Features Of OSB

Just for fun I’m gonna clean up OSB and make it official. I’ll add features like commenting on a post via email, each post is a little mailing list, and handling image and code attachments. For the code attachments I’ll colorize them with Pygments and put them up with the post.

Mailing Lists and Failmail

I’ll be bringing back the mailing list example soon, and I’ll also make an address failmail@lamsonproject.org. More on that later.

Python 2.6 Support

If you’re running Python 2.6 then you’ll want to wait for the 0.9.3 release later today. The deployed OSB sample at oneshotblog.com is running on Python 2.6, so I had to fix about 4 failing tests regarding unicode issues and weirdness with python-daemonize.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-04.txt0000644000076500000240000000462611211760244023347 0ustar zedshawstaffTitle: OneShotBlog Sample (Hack) Running I *finally* got off my ass and put the "OneShotBlog":http://oneshotblog.com/ sample up. This is the code (plus a few little tweaks) from the sample that is in the "Lamson source":/releases/ running on another server. It's using all the features of Lamson, include the new "Queue Receiver":/docs/deferred_processing_to_queues.html functionality. So far it's working great considering I've just been hacking on it on the side to try out the usability of the Lamson "APIs":/docs/api/ and not really taken it seriously. The concept of the oneshotblog is a little weird: # You send an email to SOMETAG@oneshotblog.com, it can have textile markup. ## SOMETAG can be anything that's a tag (chars, ints, periods, no leading periods), like: i.want.my.blog@oneshotblog.com # That is posted for you at http://oneshotblog.com/posts/YOURADDRESS/SOMETAG.html ## I *do* block attempts to use a bad path by whitelisting. If you send something messed up for the tag it's ignored. # Then your post is summarized and tossed on the "front page":http://oneshotblog.com/ with the most recent 50 shown. ## This is done by a deferred queue receiver that indexes messages as it receives them. # If you send to that same tag, it replaces it with the new contents (although the index will show the old stuff for a while). There's some dumb bugs in it currently, like delete doesn't remove your message from the index and other fun things, but so far it's running as expected given the work I put into it. Anyway, feel free to try it out and shoot me feedback. Try sending really broken email too so that I can see what Lamson does with it. h2. Future Features Of OSB Just for fun I'm gonna clean up OSB and make it official. I'll add features like commenting on a post via email, each post is a little mailing list, and handling image and code attachments. For the code attachments I'll colorize them with Pygments and put them up with the post. h2. Mailing Lists and Failmail I'll be bringing back the mailing list example soon, and I'll also make an address failmail@lamsonproject.org. More on that later. h2. Python 2.6 Support If you're running Python 2.6 then you'll want to wait for the 0.9.3 release later today. The deployed OSB sample at "oneshotblog.com":http://oneshotblog.com/ is running on Python 2.6, so I had to fix about 4 failing tests regarding unicode issues and weirdness with python-daemonize. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-06.html0000644000076500000240000003201411230770144023467 0ustar zedshawstaff LamsonProject: Lamson 0.9.3 Is Out And Sexy As Hell

Lamson 0.9.3 Is Out And Sexy As Hell

This release is the result of me working on my little oneshotblog.com project while tweaking and refining Lamson as I go. The end result is 0.9.3 didn’t have a lot of big code changes, but all the tiny little changes add up to a very nice release. The highlights of this release are more secure server runs, better character encoding handling for headers, various cleanups in how mail is queued, and fixes for Python 2.6 support.

My plan is to keep using this release, improving what is there currently and only adding minimal features, and just making what is there tighter. The goal is to have a 1.0 release out once I’ve used it on oneshotblog.com and the new lamsonproject.org mailing lists application.

Getting It (As Usual)

All you need to do to get lamson is read the download instruction and then go through the getting started to try it out.

The docs may be out of date with this release, but I’m going through docs tomorrow.

NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM

I’ll be presenting Lamson at the NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM and helping people get started with it. If you wanted to try it but didn’t want to waste the time then come down and I’ll hook you up.

OneShotBlog Sample Is Live

Lamson includes the source to the oneshotblog.com project in the examples/osb directory. You can grab the source tar.gz and look in there fore the source. You should also try out the sample site to help test out Lamson, especially if you use a weird character set (since that’s the biggest weakness currently).

This release of OSB features a bunch of bug fixes, tons of tests that improve the code coverage, the ability to comment on a post by sending an email, decent spam filtering of the comments, ability to flag a comment as spam, and lots of little tweaks. It uses all of the features Lamson has right now.

It still has some work to do, and I want to add the ability to send pictures and source code (with syntax highlight).

The ChangeLog

The change log for this release is (with the big ones bold):

  • Cleaned up some stray printing and logging.
  • Now prints a nicer message if args get a bad argument.
  • Managed to get a file object out of the maildir which removes the need to reconvert from a message.
  • A few minor test suite tweaks to get coverage on the most recent features.
  • Implemented a reorg of how the servers are started so that they can bind port 25 and then drop priv to a safer user.
  • Initial support for an undeliverable queue and a forwarding handler that you can run in a queue receiver to deliver unhandled mail to the relay. Needs tests.
  • Using the new Router.UNDELIVERABLE_QUEUE feature to have bad mail go to a queue.
  • Fixed the queue_command so that it prints what queue it is working with.
  • Added a setting for the Router to tell it to dump undeliverable mail to a queue.
  • First cut at semi-automated header encoding conversion.
  • Initial support for sort-of-automated header decoding/encoding on MailRequest.
  • Found a bug in clear_queue which made it not able to clear alternate queues.
  • Small changes to fix a non-idiomatic has_key usage.
  • Tweaked testing so that it resets the Router state when you do a client.begin() in unit tests.
  • Tweaked the boot vs. test logging setup to make it nicer for both situations.
  • Since the Router is the primary code loading mechanism, it now uses its own separate logger that you can redirect to stderr to see loading errors during dev.
  • Created a disconnected set of config testing gear so that it can test without relying on any examples. Also configures the logger using a config file and cleans up test output.
  • Added a feature to the routes command to print out how each matching regex will match a given test address.
  • Added an exception handler for loading and reloading handlers so that you can see which ones are broke during development.
  • Added a -test feature to lamson routes that let’s you test an address to see what routes it matches.
  • Updated for Python 2.6 so the tests will run, not sure why they pass in Python 2.5 actually.
  • Modification to the mail parsing so that it forces 'ascii’ encoding to keep email.message_from happy in Python 2.6

Using The Drop Privilege

Lamson can now drop privilege after binding port 25. Previously it was too difficult to make the python-daemonize library do it correctly, but after I reorganized some code I was able to do the privilege drop manually to implement it. What this means is you can run your email server and stop being root right after you bind port 25.

To do this, you’ll just run your server with:

$ sudo lamson start -uid 500 -gid 500
$ cd ..
$ sudo chown -R someuser.someuser yourapp

You technically don’t have to fix the permissions, but if you use queue receivers then you’ll need to change the permissions back. Lamson still creates a few files as root before dropping to your given UID and GID, but hopefully it will stop doing this soon.

Obviously, you might want to know what your UID and GID is, but it’s important that you know what Python thinks it is:

Python 2.5.1 (r251:54863, Jan 13 2009, 10:26:13) 
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getgid()
500
>>> os.getuid()
500
>>> 

There’s other ways to find it out, but that’s the Python way.

Using lamson routes

The lamson routes command has improved a bit so that you can inspect your routing and test out an email address to see how it would be sorted. Here’s a sample run:

osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 

If you’re working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it’ll take an email address and tell you all the routes that match it.

The output needs to be cleaned up, but for debugging this is nice.

The lamson web Command

While working on oneshotblog.com I kept having to open up the site that it generates, so I just threw a dirt simple HTTP server into Lamson. Here’s a sample of running it:

~ $ cd projects/lamson/examples/osb/
osb $ lamson web -basedir app/data/
Starting server on 127.0.0.1:8888 out of directory 'app/data/'
localhost - - [06/Jun/2009 12:31:11] "GET / HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/reset.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/main.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:18] code 404, message File not found

It just stays there running and printing to the console, so I just use GNU screen and leave it running in one of the windows.

As Usual Test And Report

Test this release out, and shoot me feedback. I’m hanging on irc.freenode.org again in the #lamson room, so just stop in if you need help.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-06.txt0000644000076500000240000002004311213057130023333 0ustar zedshawstaffTitle: Lamson 0.9.3 Is Out And Sexy As Hell This release is the result of me working on my little "oneshotblog.com":http://oneshotblog.com/ project while tweaking and refining Lamson as I go. The end result is 0.9.3 didn't have a lot of big code changes, but all the tiny little changes add up to a very nice release. The highlights of this release are more secure server runs, better character encoding handling for headers, various cleanups in how mail is queued, and fixes for Python 2.6 support. My plan is to keep using this release, improving what is there currently and only adding minimal features, and just making what is there tighter. The goal is to *have a 1.0 release out* once I've used it on "oneshotblog.com":http://oneshotblog.com/ and the new "lamsonproject.org mailing lists":/lists/ application. h2. Getting It (As Usual) All you need to do to get lamson is read the "download instruction":/download.html and then go through the "getting started":/docs/getting_started.html to try it out. The *docs may be out of date with this release*, but I'm going through docs tomorrow. h2. NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM I'll be presenting Lamson at the "NYLUG Python Workshop Tuesday June 09 6:00PM-8:00PM":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html and helping people get started with it. If you wanted to try it but didn't want to waste the time then come down and I'll hook you up. h2. OneShotBlog Sample Is Live Lamson includes the source to the "oneshotblog.com":http://oneshotblog.com/ project in the @examples/osb@ directory. You can "grab the source tar.gz":/releases/ and look in there fore the source. You should also try out the sample site to help test out Lamson, especially if you use a weird character set (since that's the biggest weakness currently). This release of OSB features a bunch of bug fixes, tons of tests that improve the code coverage, the ability to comment on a post by sending an email, decent spam filtering of the comments, ability to flag a comment as spam, and lots of little tweaks. It uses *all* of the features Lamson has right now. It still has some work to do, and I want to add the ability to send pictures and source code (with syntax highlight). h2. The ChangeLog The change log for this release is (with the big ones bold): * Cleaned up some stray printing and logging. * Now prints a nicer message if args get a bad argument. * *Managed to get a file object out of the maildir which removes the need to reconvert from a message.* * A few minor test suite tweaks to get coverage on the most recent features. * *Implemented a reorg of how the servers are started so that they can bind port 25 and then drop priv to a safer user.* * *Initial support for an undeliverable queue and a forwarding handler that you can run in a queue receiver to deliver unhandled mail to the relay. Needs tests.* * Using the new Router.UNDELIVERABLE_QUEUE feature to have bad mail go to a queue. * Fixed the queue_command so that it prints what queue it is working with. * Added a setting for the Router to tell it to dump undeliverable mail to a queue. * *First cut at semi-automated header encoding conversion.* * Initial support for sort-of-automated header decoding/encoding on MailRequest. * Found a bug in clear_queue which made it not able to clear alternate queues. * Small changes to fix a non-idiomatic has_key usage. * Tweaked testing so that it resets the Router state when you do a client.begin() in unit tests. * Tweaked the boot vs. test logging setup to make it nicer for both situations. * *Since the Router is the primary code loading mechanism, it now uses its own separate logger that you can redirect to stderr to see loading errors during dev.* * *Created a disconnected set of config testing gear so that it can test without relying on any examples. Also configures the logger using a config file and cleans up test output.* * *Added a feature to the routes command to print out how each matching regex will match a given test address.* * Added an exception handler for loading and reloading handlers so that you can see which ones are broke during development. * Added a -test feature to lamson routes that let's you test an address to see what routes it matches. * *Updated for Python 2.6 so the tests will run, not sure why they pass in Python 2.5 actually.* * Modification to the mail parsing so that it forces 'ascii' encoding to keep email.message_from happy in Python 2.6 h2. Using The Drop Privilege Lamson can now drop privilege after binding port 25. Previously it was too difficult to make the python-daemonize library do it correctly, but after I reorganized some code I was able to do the privilege drop manually to implement it. What this means is you can run your email server and *stop being root* right after you bind port 25. To do this, you'll just run your server with:
$ sudo lamson start -uid 500 -gid 500
$ cd ..
$ sudo chown -R someuser.someuser yourapp
You technically don't have to fix the permissions, but if you use "queue receivers":/docs/deferred_processing_to_queues.html then you'll need to change the permissions back. Lamson still creates a few files as root before dropping to your given UID and GID, but hopefully it will stop doing this soon. Obviously, you might want to know what your UID and GID is, but it's important that you know what *Python* thinks it is:
Python 2.5.1 (r251:54863, Jan 13 2009, 10:26:13) 
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getgid()
500
>>> os.getuid()
500
>>> 
There's other ways to find it out, but that's the Python way. h2. Using lamson routes The @lamson routes@ command has improved a bit so that you can inspect your routing and test out an email address to see how it would be sorted. Here's a sample run:
osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 
If you're working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it'll take an email address and tell you all the routes that match it. The output needs to be cleaned up, but for debugging this is nice. h2. The lamson web Command While working on "oneshotblog.com":http://oneshotblog.com/ I kept having to open up the site that it generates, so I just threw a dirt simple HTTP server into Lamson. Here's a sample of running it:
~ $ cd projects/lamson/examples/osb/
osb $ lamson web -basedir app/data/
Starting server on 127.0.0.1:8888 out of directory 'app/data/'
localhost - - [06/Jun/2009 12:31:11] "GET / HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/reset.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:11] "GET /styles/main.css HTTP/1.1" 200 -
localhost - - [06/Jun/2009 12:31:18] code 404, message File not found
It just stays there running and printing to the console, so I just use "GNU screen":http://www.gnu.org/software/screen/screen.html and leave it running in one of the windows. h2. As Usual Test And Report Test this release out, and shoot me feedback. I'm hanging on irc.freenode.org again in the #lamson room, so just stop in if you need help. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-08.html0000644000076500000240000001353311230770144023476 0ustar zedshawstaff LamsonProject: A Screencast And Docs On Deploying Lamson And OneShotBlog

A Screencast And Docs On Deploying Lamson And OneShotBlog

I just finished writing some new documentation on Deploying Lamson and OneShotBlog that shows you how to install Lamson and all required software into a completely clean Python 2.6 and virtualenv configuration. The instructions take you from nothing to a running oneshotblog.com installation that you can play with and hack on. The process of doing Lamson deployments is still a little too rough for me, but these instructions should get people started.

I also created the first screencast teaching you how to do the same deployment but stopping at installing oneshotblog.com to keep things short. It is basically me talking and typing into a terminal window to 12 minutes showing you how to do it. You can also download the video from archive.org to get a better viewing experience.

Being my first screencast I can say that it is very tedious. I’ll be working on various strategies to make the process more streamlined and easier since having to take a whole day for each one will drive me insane.

If you have suggestions on making the video better, let me know and also if you know of tools to help clean up video. One thing I know right away needs to improve is the sound quality. An apartment in NYC is just too damn noisy for this kind of thing.

Enjoy, and don’t forget that I’ll be presenting Lamson at the NYLUG Python workshop tomorrow June 9th at 6:00pm so come on down if you want to get your Lamson working.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-08.txt0000644000076500000240000000335511213316150023344 0ustar zedshawstaffTitle: A Screencast And Docs On Deploying Lamson And OneShotBlog I just finished writing some new documentation on "Deploying Lamson and OneShotBlog":/docs/deploying_lamson.html that shows you how to install Lamson and all required software into a completely clean Python 2.6 and virtualenv configuration. The instructions take you from nothing to a running "oneshotblog.com":http://oneshotblog.com/ installation that you can play with and hack on. The process of doing Lamson deployments is still a little too rough for me, but these instructions should get people started. I also created the first screencast "teaching you how to do the same deployment":/videos/ but stopping at installing "oneshotblog.com":http://oneshotblog.com to keep things short. It is basically me talking and typing into a terminal window to 12 minutes showing you how to do it. You can also "download the video":http://www.archive.org/details/Lamsonproject.orgDeployLamsonInAVirtualenv from "archive.org":http://archive.org/ to get a better viewing experience. Being my first screencast I can say that it is very tedious. I'll be working on various strategies to make the process more streamlined and easier since having to take a whole day for each one will drive me insane. If you have suggestions on making the video better, "let me know":/contact.html and also if you know of tools to help clean up video. One thing I know right away needs to improve is the sound quality. An apartment in NYC is just too damn noisy for this kind of thing. Enjoy, and don't forget that I'll be "presenting Lamson at the NYLUG Python workshop tomorrow June 9th at 6:00pm":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html so come on down if you want to get your Lamson working. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-09.html0000644000076500000240000001102011230770144023464 0ustar zedshawstaff LamsonProject: Lamson At NYLUG Python Workshop Today @ 6:00PM

Lamson At NYLUG Python Workshop Today @ 6:00PM

Just a quick reminder that I’ll be presenting Lamson to the NYLUG Python Workshop today at 6:00PM. The event is at the NY Public Library Hudson Park Branch, 66 Leroy St., NY NY 10014 in NYC and you can find out more here.

I’ll be showing people how to set up Lamson, get it installed, do cool stuff with it, and then just answering questions and helping people work with it.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-09.txt0000644000076500000240000000102011213542615023337 0ustar zedshawstaffTitle: Lamson At NYLUG Python Workshop Today @ 6:00PM Just a quick reminder that I'll be presenting "Lamson":http://lamsonproject.org/ to the NYLUG Python Workshop today at 6:00PM. The event is at the NY Public Library Hudson Park Branch, 66 Leroy St., NY NY 10014 in NYC and you can "find out more here.":http://nylug.org/pipermail/nylug-announce/2009-June/000795.html I'll be showing people how to set up Lamson, get it installed, do cool stuff with it, and then just answering questions and helping people work with it. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-14.html0000644000076500000240000003272711230770144023501 0ustar zedshawstaff LamsonProject: The Mailocalypse Is Upon Us!

The Mailocalypse Is Upon Us!

I’m currently polishing off the two final features before I start going for the Lamson 1.0 release. I’ve been using Lamson to make a few little cute applications and create one thing for a potential client, and so far I haven’t had to change much since 0.9.3. It’s great so far and I hope that Lamson 1.0 will be a fun release.

The big thing that must improve though is handling character encodings in emails. After spending a week or more trying to come up with an automated conversion scheme that would honor all the encodings on the planet, I had a realization.

Why Isn’t All Mail UTF-8?

Remembering the purpose of Lamson as a modern mail server and framework, it finally struck me as dumb that I’m trying to keep around encodings that were invented in the pre-Unicode days. Every modern system understands and displays UTF-8, and apart from some pissy attitudes from the Japanese about the Han unification, there’s really no reason that every email Lamson handles can’t be “upgraded” to UTF-8.

As I thought about this more I started to like the idea. Take all mail Lamson handles, and upgrade it to utf-8. Then inside Lamson you know that it’s future proofed and ready to be used by Python no matter what. Finally when the mail goes out, the conversion for sending is simply to encode the email as proper UTF-8 encoding consistently.

However, I’m not the type of person to think any random idea I have is instantly valid because I thought it up. Before I make a grand announcement that Lamson only deals with UTF-8 I better know that it’s going to have minimal impact on people using it. I made a similar decision with Mongrel, where I chose not to accept clients that didn’t follow the standard, and it turned out to block a ton of security attacks for free.

What if Lamson’s required conversion to UTF-8 could not only simplify Lamson mail processing, but also prevent spam and security flaws? I decided to do some basic analysis and find out.

This Is The Mailocalypse

I’ve decided that Lamson will NOT try to maintain the encodings it is given by a client. It will consider all encodings suspect, and attempt to convert them to UTF-8 as a means of cleaning and simplifying the mail it receives before it talks to other systems. Every other system you’d most likely talk to is either UTF-8 capable or even requires it.

I’m calling this “The Mailocalypse” because it’s funny, but it will also separate Lamson from other systems, in potentially awesome or horrible ways.

To validate this premise, I created a small Python script that takes a Maildir or mbox and tries to convert every email into a UTF-8 encoding. It’s not trying to write the emails, just load them and convert every single piece reliably to UTF-8. It then reports any emails that have a problem with the conversion. After that I ran it on a bunch of mail I had and got other people to run it and report their findings.

You can grab the mailocalypse.py script here. Improvements welcome.

The data I got from a small sample of the mail (randomly selected) was the following:

all bad spam
5069 7 0
263 15 1
1806 231 1
2650 69 0
4509 20 0
2023 495 1
3723 74 0

all is the total number of messages in the mailbox (including bad). bad is the number that were bad. spam is whether that box was a spam box or not (meaning all messages were classified as spam).

I also made sure that the characters showed up in web browsers and in various UTF-8 mail clients and terminal windows. Most of the conversion was reliable and very fast for a quick script.

With the above data, there’s two things you can notice right away, and which are confirmed with a quick R session:

  1. Spam fails to convert more frequently than ham messages. It looks like failure to convert is a 0.03 significant indicator that the message is spam. I’d need more data to confirm that.
  2. Legit mail (ham) only has a 1.2% chance of failing to convert, and a quick eye-ball sample says those failures mail are mostly spam that wasn’t classified right.

Potential Problems

This analysis shows initially that converting mail to UTF-8 could work, but there are two problems I see so far:

  1. I may not be doing the best conversion in that script.
  2. Encryption and signatures will screw this up.

For the first problem I’m posting the script and asking people to check it out and let me know if there’s problems. Run it on your stuff and shoot me the ALL/BAD numbers when it’s done. Check the code and see if you can improve it. Whatever you can to show that converting to UTF-8 is a good OR bad idea.

A key design decision though will be that you never lose the original email, only that it is converted for Lamson so you can work with it in your code without it blowing up. If the mail can’t convert, then it’s bad email and should be rejected anyway. No point trying to process it. For encrypted email or ones with signatures you would just run all the validation on the original, or forward the original on, depending on your application.

For example, a mailing list would still want to convert all mail to UTF-8 for processing, posting to web sites, spam filtering, and rejecting potentially bad email. Signed and encrypted email would then just be forwarded on in original form, or decoded and validated depending on the how the mailing list works.

Advice And Criticism Welcome

Take the mailocalypse script (presented below for review) and try it out. Confirm that it works on most of your mail, and send me the results. Simplest way is to send me a message on twitter like:

@zedshaw ALL 2340 BAD 17 spam yes #mailocalypse

And I’ll tally the results and do a better analysis.

You are also encouraged to fill me in on what I’m missing in the idea. Realize that part of the idea is to eliminate uncommon corner cases and to prove that a situation is common using evidence. Try to follow the same model by running the script on your mail to prove that it won’t convert reliably or devise your own script to demonstrate your thoughts.

The Codes

You can grab the mailocalypse.py script here. Improvements welcome.


import email
from email.header import make_header, decode_header
from string import capwords
import sys
import mailbox

ALL_MAIL = 0
BAD_MAIL = 0

def all_parts(msg):
    parts = [m for m in msg.walk() if m != msg]

    if not parts:
        parts = [msg]

    return parts

def collapse_header(header):
    if header.strip().startswith("=?"):
        decoded = decode_header(header)
        converted = (unicode(
            x[0], encoding=x[1] or 'ascii', errors='replace')
            for x in decoded)
        value = u"".join(converted)
    else:
        value = unicode(header, errors='replace')

    return value.encode("utf-8")

def convert_header_insanity(header):
    if header is None: 
        return header
    elif type(header) == list:
        return [collapse_header(h) for h in header]
    else:
        return collapse_header(header)

def encode_header(name, val, charset='utf-8'):
    msg[name] = make_header([(val, charset)]).encode()

def bless_headers(msg):
    # go through every header and convert it to utf-8
    headers = {}

    for h in msg.keys():
        headers[capwords(h, '-')] = convert_header_insanity(msg[h])

    return headers

def dump_headers(headers):
    for h in headers:
        print h, headers[h]

def mail_load_cleanse(msg_file):
    global ALL_MAIL
    global BAD_MAIL

    msg = email.message_from_file(msg_file)
    headers = bless_headers(msg)

    # go through every body and convert it to utf-8
    parts = all_parts(msg)
    bodies = []
    for part in parts:
        guts = part.get_payload(decode=True)
        if part.get_content_maintype() == "text":
            charset = part.get_charsets()[0]
            try:
                if charset:
                    uguts = unicode(guts, part.get_charsets()[0])
                    guts = uguts.encode("utf-8")
                else:
                    guts = guts.encode("utf-8")
            except UnicodeDecodeError, exc:
                print >> sys.stderr, "CONFLICTED CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except LookupError, exc:
                print >> sys.stderr, "UNKNOWN CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except Exception, exc:
                print >> sys.stderr, "WEIRDO ERROR", exc, part.get_charsets()
                BAD_MAIL += 1

            ALL_MAIL += 1

mb = None

try:
    mb = mailbox.Maildir(sys.argv[1])
    len(mb)  # need this to make the maildir try to read the directory and fail
except OSError:
    print "NOT A MAILDIR, TRYING MBOX"
    mb = mailbox.mbox(sys.argv[1])

if not mb:
    print "NOT A MAILDIR OR MBOX, SORRY"

for key in mb.keys():
    mail_load_cleanse(mb.get_file(key))

print >> sys.stderr, "ALL", ALL_MAIL
print >> sys.stderr, "BAD", BAD_MAIL


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-14.txt0000644000076500000240000002157011215276254023354 0ustar zedshawstaffTitle: The Mailocalypse Is Upon Us! I'm currently polishing off the two final features before I start going for the Lamson 1.0 release. I've been using Lamson to make a few little cute applications and create one thing for a potential client, and so far I haven't had to change much since 0.9.3. It's great so far and I hope that Lamson 1.0 will be a fun release. The *big* thing that must improve though is handling character encodings in emails. After spending a week or more trying to come up with an automated conversion scheme that would honor all the encodings on the planet, I had a realization. h2. Why Isn't All Mail UTF-8? Remembering the purpose of Lamson as a *modern* mail server and framework, it finally struck me as dumb that I'm trying to keep around encodings that were invented in the pre-Unicode days. Every modern system understands and displays UTF-8, and apart from some pissy attitudes from the Japanese about the Han unification, there's really no reason that every email Lamson handles can't be "upgraded" to UTF-8. As I thought about this more I started to like the idea. Take all mail Lamson handles, and upgrade it to utf-8. Then inside Lamson you know that it's future proofed and ready to be used by Python no matter what. Finally when the mail goes out, the conversion for sending is simply to encode the email as proper UTF-8 encoding consistently. However, I'm not the type of person to think any random idea I have is instantly valid because I thought it up. Before I make a grand announcement that Lamson only deals with UTF-8 I better know that it's going to have minimal impact on people using it. I made a similar decision with Mongrel, where I chose *not* to accept clients that didn't follow the standard, and it turned out to block a ton of security attacks for free. What if Lamson's required conversion to UTF-8 could not only simplify Lamson mail processing, but also prevent spam and security flaws? I decided to do some basic analysis and find out. h2. This Is The Mailocalypse I've decided that Lamson will *NOT* try to maintain the encodings it is given by a client. It will consider all encodings suspect, and attempt to convert them to UTF-8 as a means of cleaning and simplifying the mail it receives before it talks to other systems. Every other system you'd most likely talk to is either UTF-8 capable or even requires it. I'm calling this "The Mailocalypse" because it's funny, but it will also separate Lamson from other systems, in potentially awesome or horrible ways. To validate this premise, I created a small Python script that takes a Maildir or mbox and tries to convert every email into a UTF-8 encoding. It's not trying to write the emails, just load them and convert every single piece reliably to UTF-8. It then reports any emails that have a problem with the conversion. After that I ran it on a bunch of mail I had and got other people to run it and report their findings. bq. "You can grab the mailocalypse.py script here.":/mailocalypse.py Improvements welcome. The data I got from a small sample of the mail (randomly selected) was the following:
all bad spam
5069 7 0
263 15 1
1806 231 1
2650 69 0
4509 20 0
2023 495 1
3723 74 0
bq. all is the total number of messages in the mailbox (including bad). bad is the number that were bad. spam is whether that box was a spam box or not (meaning all messages were classified as spam). I also made sure that the characters showed up in web browsers and in various UTF-8 mail clients and terminal windows. Most of the conversion was reliable and very fast for a quick script. With the above data, there's two things you can notice right away, and which are confirmed with a quick R session: # Spam fails to convert more frequently than ham messages. It looks like failure to convert is a 0.03 significant indicator that the message is spam. I'd need more data to confirm that. # Legit mail (ham) only has a 1.2% chance of failing to convert, and a quick eye-ball sample says those failures mail are mostly spam that wasn't classified right. h2. Potential Problems This analysis shows initially that converting mail to UTF-8 could work, but there are two problems I see so far: # I may not be doing the best conversion in that script. # Encryption and signatures will screw this up. For the first problem I'm posting the script and asking people to check it out and let me know if there's problems. Run it on your stuff and shoot me the ALL/BAD numbers when it's done. Check the code and see if you can improve it. Whatever you can to show that converting to UTF-8 is a good *OR* bad idea. A key design decision though will be that you never lose the original email, only that it is converted for Lamson so you can work with it in your code without it blowing up. If the mail can't convert, then it's bad email and should be rejected anyway. No point trying to process it. For encrypted email or ones with signatures you would just run all the validation on the original, or forward the original on, depending on your application. For example, a mailing list would still want to convert all mail to UTF-8 for processing, posting to web sites, spam filtering, and rejecting potentially bad email. Signed and encrypted email would then just be forwarded on in original form, or decoded and validated depending on the how the mailing list works. h2. Advice And Criticism Welcome Take the mailocalypse script (presented below for review) and try it out. Confirm that it works on most of your mail, and send me the results. Simplest way is to send me a message on twitter like: bq. @zedshaw ALL 2340 BAD 17 spam yes #mailocalypse And I'll tally the results and do a better analysis. You are also encouraged to fill me in on what I'm missing in the idea. Realize that *part* of the idea is to eliminate uncommon corner cases and to prove that a situation is common using evidence. Try to follow the same model by running the script on your mail to prove that it won't convert reliably or devise your own script to demonstrate your thoughts. h2. The Codes "You can grab the mailocalypse.py script here.":/mailocalypse.py Improvements welcome.

import email
from email.header import make_header, decode_header
from string import capwords
import sys
import mailbox


ALL_MAIL = 0
BAD_MAIL = 0


def all_parts(msg):
    parts = [m for m in msg.walk() if m != msg]
    
    if not parts:
        parts = [msg]

    return parts

def collapse_header(header):
    if header.strip().startswith("=?"):
        decoded = decode_header(header)
        converted = (unicode(
            x[0], encoding=x[1] or 'ascii', errors='replace')
            for x in decoded)
        value = u"".join(converted)
    else:
        value = unicode(header, errors='replace')

    return value.encode("utf-8")


def convert_header_insanity(header):
    if header is None: 
        return header
    elif type(header) == list:
        return [collapse_header(h) for h in header]
    else:
        return collapse_header(header)


def encode_header(name, val, charset='utf-8'):
    msg[name] = make_header([(val, charset)]).encode()


def bless_headers(msg):
    # go through every header and convert it to utf-8
    headers = {}

    for h in msg.keys():
        headers[capwords(h, '-')] = convert_header_insanity(msg[h])

    return headers

def dump_headers(headers):
    for h in headers:
        print h, headers[h]

def mail_load_cleanse(msg_file):
    global ALL_MAIL
    global BAD_MAIL

    msg = email.message_from_file(msg_file)
    headers = bless_headers(msg)

    # go through every body and convert it to utf-8
    parts = all_parts(msg)
    bodies = []
    for part in parts:
        guts = part.get_payload(decode=True)
        if part.get_content_maintype() == "text":
            charset = part.get_charsets()[0]
            try:
                if charset:
                    uguts = unicode(guts, part.get_charsets()[0])
                    guts = uguts.encode("utf-8")
                else:
                    guts = guts.encode("utf-8")
            except UnicodeDecodeError, exc:
                print >> sys.stderr, "CONFLICTED CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except LookupError, exc:
                print >> sys.stderr, "UNKNOWN CHARSET:", exc, part.get_charsets()
                BAD_MAIL += 1
            except Exception, exc:
                print >> sys.stderr, "WEIRDO ERROR", exc, part.get_charsets()
                BAD_MAIL += 1


            ALL_MAIL += 1

mb = None

try:
    mb = mailbox.Maildir(sys.argv[1])
    len(mb)  # need this to make the maildir try to read the directory and fail
except OSError:
    print "NOT A MAILDIR, TRYING MBOX"
    mb = mailbox.mbox(sys.argv[1])

if not mb:
    print "NOT A MAILDIR OR MBOX, SORRY"

for key in mb.keys():
    mail_load_cleanse(mb.get_file(key))

print >> sys.stderr, "ALL", ALL_MAIL
print >> sys.stderr, "BAD", BAD_MAIL

lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-20.html0000644000076500000240000002123311230770144023464 0ustar zedshawstaff LamsonProject: Lamson 0.9.4 With Unicode Super Powers

Lamson 0.9.4 With Unicode Super Powers

Lamson 0.9.4 is out and it’s sporting a completely rewritten and meticulously crafted encoding system. With the new lamson.encoding code Lamson can now decode nearly any nasty horrible encoded spam or mail you hand it, turn it into pristine nice Python unicode strings, and then output sweet clean ascii or utf-8 in a consistent way.

The purpose of this new encoding system is to make sure that Lamson is giving your handlers the best input it can, based on the assumption that the world is evil and Lamson will be handed utter garbage.

In order to pull this off, Lamson actually attempts to decode everything it gets, and uses chardet to guess whenever it can’t. It goes far enough to make sure that 99.9% of legit mail gets through, and the rest is usually spam. However, Lamson is so good at this conversion now that it even does it properly on about 99% of spam, even more.

It doesn’t matter what character encoding is used, if there’s a mix of encodings or even if something lies about the encoding or doesn’t give one. Lamson can figure this out and convert it anyway, and the rest is junk.

You can look at this image of a spam inbox and this image of a spam inbox where you can see a screen session showing this off. This is a split screen with a Mutt on top showing badly formatted spam, and then a Mutt on the bottom with the results of Lamson’s cleansing. This is just running in iTerm, but looks the same in most unicode enabled terminals and clients.

The interesting side effect of Lamson’s decoding is that it undoes almost all of the spam obfuscation techniques quickly and consistently.

There Will Be Bugs

I’ve thrashed the hell out of this code and made sure that I really took the time to get it right. I’ve ran it on thousands and thousands of real and spam mail and tested the crap out of it with unit tests and fuzzing.

Still, this is new code handling email so there will be bugs in it. If you run into an email that you think should parse correctly, send it to me and I’ll look at it.

How It Works

All Lamson does is completely parse every header and figure out how to decode it, but it assumes that the client is a liar. If the string decodes without errors then Lamson is happy, but if it blows up then Lamson uses chardet to figure out what the contents really are encoded as. If chardet can’t figure it out, or if it’s still busted, then Lamson rejects that mail (which happens less than a fraction of a percent of the time to real email).

Once this decoding process is finished Lamson has converted the email completely into Python unicode only. You can work with it in a modern way without worrying about the original codecs, and you can set your headers and attachments without worrying about setting the codecs because Lamson will use the same tech to get the encoding right.

When Lamson writes an email, it assumes that your text can be encoded as either plain ASCII (as most headers are), or UTF-8. If it can’t, then your text probably can’t be processed by Lamson anyway. Lamson will favor ASCII first since it’s easier, and then use UTF-8 for anything that can’t be ASCII.

During this conversion process anything that’s not text is treated as raw binary and just decoded as-is.

By doing this Lamson produces nice clean email that can be easily processed and passed around, and which you can review and debug easier. It also turns Lamson into a “modernizing” agent since it is producing the email it wants to see.

Getting This Release

I had some issues pushing this release to PyPI, so let me know if it barfs on you.

Otherwise, you can hit the download section and grab the gear.

The New “Cleanse” Command

In order to make it easier for people to try this new cleansing system on real email I’ve written a quick lamson cleanse command. What this command will do is take a mbox or Maildir, and run it through the washing machine, writing the results to a different maildir. When it’s done you can go look at the reasons why some mail failed, how many failures, and you can actually open that maildir mail and look at it with your client.

Try it out and report any errors you find.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-20.txt0000644000076500000240000001045011217126716023343 0ustar zedshawstaffTitle: Lamson 0.9.4 With Unicode Super Powers Lamson 0.9.4 is out and it's sporting a completely rewritten and meticulously crafted encoding system. With the new "lamson.encoding":http://lamsonproject.org/docs/api/lamson.encoding-module.html code Lamson can now decode nearly any nasty horrible encoded spam or mail you hand it, turn it into pristine nice Python unicode strings, and then output sweet clean ascii or utf-8 in a consistent way. The purpose of this new encoding system is to make sure that Lamson is giving your handlers the best input it can, based on the assumption that the world is evil and Lamson will be handed utter garbage. In order to pull this off, Lamson actually attempts to decode everything it gets, and uses "chardet":http://chardet.feedparser.org/ to guess whenever it can't. It goes far enough to make sure that 99.9% of legit mail gets through, and the rest is usually spam. However, Lamson is so good at this conversion now that it even does it properly on about 99% of spam, even more. It doesn't matter what character encoding is used, if there's a mix of encodings or even if something lies about the encoding or doesn't give one. Lamson can figure this out and convert it anyway, and the rest is junk. You can look at this "image of a spam inbox":/lamson_vs_spam.png and "this image of a spam inbox":/lamson_vs_spam_2.png where you can see a screen session showing this off. This is a split screen with a Mutt on top showing badly formatted spam, and then a Mutt on the bottom with the results of Lamson's cleansing. This is just running in iTerm, but looks the same in most unicode enabled terminals and clients. The interesting side effect of Lamson's decoding is that it undoes almost all of the spam obfuscation techniques quickly and consistently. h2. There Will Be Bugs I've thrashed the hell out of this code and made sure that I really took the time to get it right. I've ran it on thousands and thousands of real and spam mail and tested the crap out of it with unit tests and fuzzing. Still, this is new code handling email so there will be bugs in it. If you run into an email that you think should parse correctly, send it to me and I'll look at it. h2. How It Works All Lamson does is completely parse every header and figure out how to decode it, but it assumes that the client is a liar. If the string decodes without errors then Lamson is happy, but if it blows up then Lamson uses chardet to figure out what the contents really are encoded as. If chardet can't figure it out, or if it's still busted, then Lamson rejects that mail (which happens less than a fraction of a percent of the time to real email). Once this decoding process is finished Lamson has converted the email completely into Python unicode only. You can work with it in a modern way without worrying about the original codecs, and you can set your headers and attachments without worrying about setting the codecs because Lamson will use the same tech to get the encoding right. When Lamson writes an email, it assumes that your text can be encoded as either plain ASCII (as most headers are), or UTF-8. If it can't, then your text probably can't be processed by Lamson anyway. Lamson will favor ASCII first since it's easier, and then use UTF-8 for anything that can't be ASCII. bq. During this conversion process anything that's *not* text is treated as raw binary and just decoded as-is. By doing this Lamson produces nice clean email that can be easily processed and passed around, and which you can review and debug easier. It also turns Lamson into a "modernizing" agent since it is producing the email it wants to see. h2. Getting This Release I had some issues pushing this release to PyPI, so let me know if it barfs on you. Otherwise, you can hit the "download":/download.html section and grab the gear. h2. The New "Cleanse" Command In order to make it easier for people to try this new cleansing system on real email I've written a quick @lamson cleanse@ command. What this command will do is take a mbox or Maildir, and run it through the washing machine, writing the results to a different maildir. When it's done you can go look at the reasons why some mail failed, how many failures, and you can actually open that maildir mail and look at it with your client. Try it out and report any errors you find. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-22.html0000644000076500000240000002040511230770144023466 0ustar zedshawstaff LamsonProject: 0.9.5 Almost There, But Stumped On Templates

0.9.5 Almost There, But Stumped On Templates

Since the 0.9.4 release I’ve rewritten the main part of the decoding parser so that it’s much cleaner and handles more edge conditions. If there’s one word that defines what makes MIME horrible it would be “edge”.

It’s amazing the kind of stupid crap clients are allowed to send out in headers. I’ve actually got test cases from Mutt where it breaks headers up into completely different encodings across multiple lines for no damn reason at all. Here’s a test I had to write just to cover the one worst case I found:

def test_dumb_shit():
    # this is a sample of possibly the worst case Mutt can produce
    idiot = '=?iso-8859-1?B?SOlhdnkgTel05WwgVW7uY/hk?=\n\t=?iso-8859-1?Q?=E9?='
    should_be = u'H\xe9avy M\xe9t\xe5l Un\xeec\xf8d\xe9'
    assert_equal(encoding.header_from_mime_encoding(idiot), should_be)

If you can decode what that actually is and why it’s retarded then you’ll win a prize. I’ll give you a hint: B != Q.

The example of the above working is on this page at oneshotblog.com so that’s solved (for now).

Now About Those Templates

I originally wanted to make writing templates as easy as writing an email or a web page, so I let you do things like this:

To: {{ message['from'] }}
From: {{ confirm_address }}@{{host}}
Subject:  Please confirm your new blog {{ post_name }}
Reply-To: {{ confirm_address }}@{{host}}

Hi there, you requested that I create a blog with a post named:

{{ post_name }} : {{ message['subject'] }}
...

This is a Jinja2 that oneshotblog.com sends out when it makes you confirm creating a blog.

This seems all normal, since it’s the headers you want and the body, like an email, but it is a total fantasy. The reason is you can’t actually decode the above template if any part of it has non-ascii chars. I’ll explain, and then hopefully I can get some suggestions on what people would like.

First, Lamson does the right thing and in your templates you are getting perfectly usable and future proof unicode objects to work with in your handlers. Also, Jinja2 has no problem with taking this unicode and processing the above template. It works fine, and Jinja2 or Mako aren’t to blame for the problem with templates now.

The problem is actually in how email has to be parsed, since the email library expects you to hand it a ascii encoded string, no unicode allowed. Nothing else. Lamson’s new code can handle the unicode fine, the problem is actually in this one function:

def respond(template, variables, html=False):
    results = render(template, variables)

    data = email.message_from_string(results.encode('ascii', 'replace'))

    if html:
        msg = mail.MailResponse(Html=data.get_payload())
    else:
        msg = mail.MailResponse(Body=data.get_payload())

    msg.update(data)

    return msg

The above function has to go, but where I’m stuck is that it’d be way more elegant to ditch this whole mess completely and remove headers from the template.

It’s too hard to write these headers by hand unless they are simple ones, and there needs to be a way to pass in more complex headers that you want to add. If I’m providing a way to pass in headers to lamson.view.respond then why not take the headers out of the template and simplify the whole rendering process?

I’d like to hear from people using Lamson or those who might have ideas on this. If taking your headers out of the template and moving them to the render call will totally screw up your day, then let me know. If it comes down to it, and I have to do it, I’ll probably write a conversion tool to help.

Also, if you have a suggestion for doing the above better but keeping the headers then let me know.

Thanks for your help folks, and I’ll be hanging out in the #lamson channel on irc.freenode.org if people want to hash out a design.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-22.txt0000644000076500000240000000776511220041004023340 0ustar zedshawstaffTitle: 0.9.5 Almost There, But Stumped On Templates Since the 0.9.4 release I've rewritten the main part of the decoding parser so that it's much cleaner and handles more edge conditions. If there's one word that defines what makes MIME horrible it would be "edge". It's amazing the kind of stupid crap clients are allowed to send out in headers. I've actually got test cases from Mutt where it breaks headers up into completely different encodings across multiple lines for no damn reason at all. Here's a test I had to write just to cover the one worst case I found:
def test_dumb_shit():
    # this is a sample of possibly the worst case Mutt can produce
    idiot = '=?iso-8859-1?B?SOlhdnkgTel05WwgVW7uY/hk?=\n\t=?iso-8859-1?Q?=E9?='
    should_be = u'H\xe9avy M\xe9t\xe5l Un\xeec\xf8d\xe9'
    assert_equal(encoding.header_from_mime_encoding(idiot), should_be)
If you can decode what that actually is and why it's retarded then you'll win a prize. I'll give you a hint: @B != Q@. The example of the above working is "on this page at oneshotblog.com":http://oneshotblog.com/posts/zedshaw@zedshaw.com/test.html so that's solved (for now). h2. Now About Those Templates I originally wanted to make writing templates as easy as writing an email or a web page, so I let you do things like this:
To: {{ message['from'] }}
From: {{ confirm_address }}@{{host}}
Subject:  Please confirm your new blog {{ post_name }}
Reply-To: {{ confirm_address }}@{{host}}

Hi there, you requested that I create a blog with a post named:

{{ post_name }} : {{ message['subject'] }}
...
This is a "Jinja2":http://jinja.pocoo.org/2/ that "oneshotblog.com":oneshotblog.com sends out when it makes you confirm creating a blog. This seems all normal, since it's the headers you want and the body, like an email, but it is *a total fantasy*. The reason is you can't actually decode the above template if any part of it has non-ascii chars. I'll explain, and then hopefully I can get some suggestions on what people would like. First, Lamson does the right thing and in your templates you are getting perfectly usable and future proof @unicode@ objects to work with in your handlers. Also, Jinja2 has no problem with taking this unicode and processing the above template. It works fine, and Jinja2 or Mako aren't to blame for the problem with templates now. The problem is actually in how email has to be parsed, since the email library expects you to hand it a @ascii@ encoded string, no unicode allowed. Nothing else. Lamson's new code can handle the unicode fine, the problem is actually in this one function:
def respond(template, variables, html=False):
    results = render(template, variables)

    data = email.message_from_string(results.encode('ascii', 'replace'))

    if html:
        msg = mail.MailResponse(Html=data.get_payload())
    else:
        msg = mail.MailResponse(Body=data.get_payload())

    msg.update(data)

    return msg
The above function has to go, but where I'm stuck is that it'd be way more elegant to ditch this whole mess completely and remove headers from the template. It's too hard to write these headers by hand unless they are simple ones, and there needs to be a way to pass in more complex headers that you want to add. If I'm providing a way to pass in headers to @lamson.view.respond@ then why not take the headers out of the template and simplify the whole rendering process? I'd like to hear from people using Lamson or those who might have ideas on this. If taking your headers out of the template and moving them to the render call will totally screw up your day, then let me know. If it comes down to it, and I have to do it, I'll probably write a conversion tool to help. Also, if you have a suggestion for doing the above better but keeping the headers then let me know. Thanks for your help folks, and I'll be hanging out in the #lamson channel on irc.freenode.org if people want to hash out a design. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-26.html0000644000076500000240000002023611230770144023474 0ustar zedshawstaff LamsonProject: Lamson 0.9.5, The Push To 1.0

Lamson 0.9.5, The Push To 1.0

I just released Lamson 0.9.5 with all the major Unicode refactoring done and working. This is an important release because 0.9.5 is where I declare that I’m pushing to a 1.0 release and the base Lamson APIs won’t change under penalty of death. In fact they can’t change because I’m using them myself in a few applications.

From now on, if an API has to change, then I’ll keep the old one around and attach a deprecation warning, and then provide a new one. You’ll then have by 1.0 to stop using it and start using the new one. Except for a situations where a bug must be fixed, I can’t see the actual Lamson API needing such a drastic change by 1.0.

What will happen between now and 1.0 is bug fixing, and adding any nice additional features people need within the current API to help build applications. This would be things like more connectors to other mail protocols (LMTP, QPMP), commands for deployment features, and possibly a way to write your own command.

So, if you’ve been waiting for the API to stabilize then 0.9.5 is the release to grab and start using. I’m currently using it on about 6 little projects and it’s working great so far, but I need more people to try it out and send me feedback on how well it works for them.

Getting This Release

As usual, you can read the download documentation to learn how to grab it.

After you grab it, go read these documents:

Then you should hit the API documentation and start writing your application. Keep your first one small and try deploying it in different ways.

Simple Virtualhost Deployment Instructions Coming Soon

The oneshotblog.com sample application is now deployed in a new virtualhost configuration on a new VPS I purchased this week. This configuration is different from the ones given in the documentation above because it allows you to run mulitple applications for multiple domains on a single machine.

I’ll be writing up how to build your own version of this configuration today, but the quick description is this:

  1. I have postfix running on port 25 now in front of Lamson.
  2. Postfix is configured to take all mail for each domain and dump it into a maildir queue for each one.
  3. The Lamson applications are then reconfigured in their config/boot.py to use a QueueReceiver to feed itself messages from its maildir.
  4. Lamson then just uses the same postfix server as its relay host like before.

The advantage of this configuration, apart from being able to easily host multiple domains, is that you can shutdown your Lamson application and when you start it back up it will catch up on the mail in the maildir. People will probably never know you did maintenance or stopped the server temporarily.

The disadvantage of this configuration is that it’s a royal pain to make postfix do this. The instructions will show you step-by-step how to pull it off, so hopefully it’ll make it easier on people who need it. However, I’m gonna tell people to start small and just use the simpler configuration “Lamson in front” configuration and leave this for the big times.

Mailing List Will Go Down

Finally, I’ll be moving the mailing list shortly, so if you send a message and don’t receive a reply then that’s why.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-06-26.txt0000644000076500000240000000724111221170627023350 0ustar zedshawstaffTitle: Lamson 0.9.5, The Push To 1.0 I just released Lamson 0.9.5 with all the major Unicode refactoring done and working. This is an important release because 0.9.5 is where I declare that I'm pushing to a 1.0 release and the base Lamson "APIs":/docs/api/ won't change under penalty of death. In fact they can't change because I'm using them myself in a few applications. From now on, if an API has to change, then I'll keep the old one around and attach a deprecation warning, and then provide a new one. You'll then have by 1.0 to stop using it and start using the new one. Except for a situations where a bug must be fixed, I can't see the actual Lamson API needing such a drastic change by 1.0. What will happen between now and 1.0 is bug fixing, and adding any nice additional features people need within the current "API":/docs/api/ to help build applications. This would be things like more connectors to other mail protocols (LMTP, QPMP), commands for deployment features, and possibly a way to write your own command. So, if you've been waiting for the API to stabilize then 0.9.5 is the release to grab and start using. I'm currently using it on about 6 little projects and it's working great so far, but I need more people to try it out and send me feedback on how well it works for them. h2. Getting This Release As usual, you can read the "download":/download.html documentation to learn how to grab it. After you grab it, go read these documents: * "Getting Started With Lamson":http://lamsonproject.org/docs/getting_started.html * "A Painless Introduction To Finite State Machines":http://lamsonproject.org/docs/introduction_to_finite_state_machines.html * "Deploying Lamson And OneShotBlog":http://lamsonproject.org/docs/deploying_lamson.html Then you should hit the "API documentation":http://lamsonproject.org/docs/api/ and start writing your application. Keep your first one small and try deploying it in different ways. h2. Simple Virtualhost Deployment Instructions Coming Soon The "oneshotblog.com":http://oneshotblog.com/ sample application is now deployed in a new virtualhost configuration on a new VPS I purchased this week. This configuration is different from the ones given in the documentation above because it allows you to run mulitple applications for multiple domains on a single machine. I'll be writing up how to build your own version of this configuration today, but the quick description is this: # I have postfix running on port 25 now *in front* of Lamson. # Postfix is configured to take all mail for each domain and dump it into a maildir queue for each one. # The Lamson applications are then reconfigured in their @config/boot.py@ to use a "QueueReceiver":http://lamsonproject.org/docs/api/lamson.server.QueueReceiver-class.html to feed itself messages from its maildir. # Lamson then just uses the same postfix server as its relay host like before. The advantage of this configuration, apart from being able to easily host multiple domains, is that you can shutdown your Lamson application and when you start it back up it will catch up on the mail in the maildir. People will probably never know you did maintenance or stopped the server temporarily. The disadvantage of this configuration is that it's a royal pain to make postfix do this. The instructions will show you step-by-step how to pull it off, so hopefully it'll make it easier on people who need it. However, I'm gonna tell people to start small and just use the simpler configuration "Lamson in front" configuration and leave this for the big times. h2. Mailing List Will Go Down Finally, I'll be moving the mailing list shortly, so if you send a message and don't receive a reply then that's why. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-03.html0000644000076500000240000001236311230770144023472 0ustar zedshawstaff LamsonProject: Article In The Reg About Lamson (By Ted Dziuba)

Article In The Reg About Lamson (By Ted Dziuba)

Just a quick update to point people at an article in The Register by Ted Dziuba entitled Lamson – email app coding without the palm sweat: Doing what Java never did. He interviewed me about Lamson, things I think you can use it for, and other fun stuffs.

A snippet from the article: “As a development platform, e-mail has gone neglected for decades. Its esoteric implementation details and specifications are regarded by many in the IT business as voodoo, best left to old-granddad programs like Sendmail or Postfix. Zed Shaw hopes to change that with his new project, Lamson. (The name Lamson is a throwback to the early 20th century pneumatic tubes used to shuttle messages between offices. It was originally called Son of Sam, but Zed’s dog convinced him to change it).”

Read the article.

0.9.6 Coming Soon

I’ve been cranking on various projects with Lamson and haven’t ran into too many other bugs. I’ll be doing a code review of what’s in the Bazaar repo and then pusing out a 0.9.6 probably this weekend.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-03.txt0000644000076500000240000000233011223705276023344 0ustar zedshawstaffTitle: Article In The Reg About Lamson (By Ted Dziuba) Just a quick update to point people at an article in "The Register":http://www.theregister.co.uk/ by "Ted Dziuba":http://teddziuba.com/ entitled "Lamson - email app coding without the palm sweat: Doing what Java never did":http://www.theregister.co.uk/2009/07/03/lamson/. He interviewed me about Lamson, things I think you can use it for, and other fun stuffs. A snippet from the article: "As a development platform, e-mail has gone neglected for decades. Its esoteric implementation details and specifications are regarded by many in the IT business as voodoo, best left to old-granddad programs like Sendmail or Postfix. Zed Shaw hopes to change that with his new project, Lamson. (The name Lamson is a throwback to the early 20th century pneumatic tubes used to shuttle messages between offices. It was originally called Son of Sam, but Zed's dog convinced him to change it)." "Read the article.":http://www.theregister.co.uk/2009/07/03/lamson/ h2. 0.9.6 Coming Soon I've been cranking on various projects with Lamson and haven't ran into too many other bugs. I'll be doing a code review of what's in the Bazaar repo and then pusing out a 0.9.6 probably this weekend. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-07.html0000644000076500000240000001016011230770144023467 0ustar zedshawstaff LamsonProject: Lamson Mailing Lists Down

Lamson Mailing Lists Down

I’m making up the release of Lamson and doing some server maintenance so I took down the Lamson server running the mailing lists. I’ll let everyone know when they’re back up.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-07.txt0000644000076500000240000000032411224721637023351 0ustar zedshawstaffTitle: Lamson Mailing Lists Down I'm making up the release of Lamson and doing some server maintenance so I took down the Lamson server running the mailing lists. I'll let everyone know when they're back up. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-09.html0000644000076500000240000003315411230770144023501 0ustar zedshawstaff LamsonProject: Lamson's Bounce Detection Algorithm

Lamson's Bounce Detection Algorithm

I just finished committing 0.9.6 code for doing bounce detection and analysis with Lamson. It’s part of the new mailing list example I’m coding up for the 0.9.6 release which I’ll be running on a free mailing list site I’m going to release soon. In this blog post I’d like to go through the bounce detection algorithm and get some feedback and samples from people. So far it works great for the samples I have, but I want it to be fairly bullet proof.

What I have here is a quick description of how Lamson is doing bounce detection, and then how it gives you parsed information to do analysis beyond that. I’m hoping that people who have to deal with bounces from systems like MS Exchange can possibly either provide samples or advice so I can tune the algorithm better.

The State of RFC 3464 And 1893

The two big RFCs for bounce handling are RFC3464 – An Extensible Message Format for Delivery Status Notifications and RFC1893 – Enhanced Mail System Status Codes which cover the headers and error messages you’ll encounter.

In RFC3464 there’s various headers that are given which a bounce message should have, as well as the double and sometimes triple multipart-mime wrapping you need to do to give back the bounced message. A major pain with this standard is that the headers are spread out across the different nested parts, some might be duplicated, and the format is never respected by the various MTAs out there.

With RFC1893 you have a large number of status codes, but they are combined in weird ways such that you have a primary code with the first digit, a secondary code with the second, and then a “third” code that’s the second and third digit combined. Unlike HTTP error codes where there’s just a single number mapped to a single status code, in RFC1893 it’s this strangely nested first+second+(second+third) thing.

Adding to this is there’s a few headers where you can put these status codes, and in various formats. Here’s a sample of the parsed Diagnostic-Code from a gmail bounce message:

 'Diagnostic-Code': [(u'smtp',
                      u'550-5.1.1',
                      u"The email account that you tried to reach does...")],

The end result is that if you want to determine if a message has bounce (not even hard vs. soft) then you have to go trolling through nested attachments looking for headers and parsing their values on the chance they might be important to the bounce.

Soft and Hard

The general consensus is that if a primary status code is greater than 4 then the message is a hard bounce, anything else is a soft bounce. Of course there’s no way this is an absolute truth, and I’m sure there will end up being various complex things people will want to do with a soft vs. a hard bounce.

Since there’s no way Lamson could know what you want to base your decisions to honor a soft vs. hard bounce, it simply lets you figure out which type it is, and then analyze the information it’s collected to determine the bounce.

Lamson’s Simplification

Lamson already converts the email into a normalized form, so the difficult part of parsing out the bounce information is simply trolling through the parts and finding all these randomly dispersed bounce headers. Then it’s a matter of parsing the values into something that can be use for analysis in code.

To simplify this, Lamson uses a dict of headers and matching regexes to search for in the email:

BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': 
        re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}

Each of these headers shows up about once, maybe more in the parts of a message that has bounced information, and they don’t show up at all or rarely in regular messages. What Lamson does is just walk through each part, finds the matches, parses with the regex, and then produces something like this:

{'Action': [(u'failed',)],
 'Content-Description': [(u'Notification',),
                         (u'Undelivered Message',),
                         (u'Delivery report',)],
 'Diagnostic-Code': [(u'smtp',
                  u'550-5.1.1',
                  u"The email account that you tried to reach does...")],
 'Final-Recipient': [(u'rfc822',
              u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')],
 'Received': [(u'by mail.zedshaw.com ...',)],
 'Remote-Mta': [(u'dns', u'gmail-smtp-in.l.google.com')],
 'Reporting-Mta': [(u'dns', u'mail.zedshaw.com')],
 'Status': [(u'5', u'1', u'1')]}

I’ve abbreviated some of the messages since they get stupidly long, but this shows you the information you get now. Lamson also provides a nice API through the lamson.bounce.BounceAnalyzer class that you’ll see in a moment.

If you know of additional headers and formats that show up in bounces, let me know.

Bounce Probability

Now, we’ve collected up all the different headers you might find in a bounce package, and we’ve attempted to parse the values into something meaningful. That means, the more of these headers that we find and successfully parse, the more likely the message is a bounce.

Lamson keeps a score while it’s finding the above headers, and for each one it finds and parses it adds a “point”. It then produces a probability <= 1.0 that the message is a bounce or not. The more headers it finds, the closer to 1.0, the higher the chance. So far it looks like any message above 0.3 is most likely a bounce message, with only a few spams that are below that.

Checking for a bounce then looks like this:

    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()

Which is from the test suite, and shows you various ways to get information about the probability of the message being a bounce.

Bounce Header Information

Finally, you’ll need to get at this parsed information to make any final decisions about the bounce. You might want to keep track of which MTAs consistently fail and include them in your black lists. You might want to tune your retries for different kind of soft bounces.

Whatever you need to do, you have the original raw headers that Lamson found, but you also have a higher level API you can use:

if msg.is_bounce():
    print "-------"
    print "hard", msg.bounce.is_hard()
    print "soft", msg.bounce.is_soft()
    print 'score', msg.bounce.score
    print 'primary', msg.bounce.primary_status
    print 'secondary', msg.bounce.secondary_status
    print 'combined', msg.bounce.combined_status
    print 'remote_mta', msg.bounce.remote_mta
    print 'reporting_mta', msg.bounce.reporting_mta
    print 'final_recipient', msg.bounce.final_recipient
    print 'diagnostic_codes', msg.bounce.diagnostic_codes
    print 'action', msg.bounce.action

This is a simple sample that prints out the various bits of information available on a bounce message. Here’s a sample of the output from the above:

hard True
soft False
score 1.0
primary (5, u'Permanent Failure')
secondary (1, u'Addressing Status')
combined (11, u'Bad destination mailbox address')
remote_mta gmail-smtp-in.l.google.com
reporting_mta mail.zedshaw.com
final_recipient asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
diagnostic_codes (u'550-5.1.1', u"The email account that you tried...")
action failed

This demonstrates how I’ve pulled out all of the error codes mentioned in RFC1893 and put them into Lamson so that you can get meaningful error messages for the various multi-level error codes you’ll encounter. All of these error codes are available in the lamson.bounce module.

Comments Or Suggestions

That’s it for the review of Lamson’s bounce detection. It’s in the early stages but already showing a lot of promise. It turned out to be much easier than I anticipated, but only after doing the work in lamson.encoding to clean up emails.

If you can think of situations that should be covered, and if you have samples of horrible bounce messages, then I’d love to have them.

Mailing Lists Coming Back

Tomorrow I’ll be using this to reimplement the mailing list sample and put it back into the bzr repository, then back online. I’ll have a nice status update about that tomorrow as I finish it up.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-09.txt0000644000076500000240000002202411225570307023351 0ustar zedshawstaffTitle: Lamson's Bounce Detection Algorithm I just finished committing 0.9.6 code for doing bounce detection and analysis with Lamson. It's part of the new mailing list example I'm coding up for the 0.9.6 release which I'll be running on a free mailing list site I'm going to release soon. In this blog post I'd like to go through the bounce detection algorithm and get some feedback and samples from people. So far it works great for the samples I have, but I want it to be fairly bullet proof. What I have here is a quick description of how Lamson is doing bounce detection, and then how it gives you parsed information to do analysis beyond that. I'm hoping that people who have to deal with bounces from systems like MS Exchange can possibly either provide samples or advice so I can tune the algorithm better. h2. The State of RFC 3464 And 1893 The two big RFCs for bounce handling are "RFC3464 - An Extensible Message Format for Delivery Status Notifications":http://www.faqs.org/rfcs/rfc3464.html and "RFC1893 - Enhanced Mail System Status Codes":http://www.faqs.org/rfcs/rfc1893.html which cover the headers and error messages you'll encounter. In RFC3464 there's various headers that are given which a bounce message should have, as well as the double and sometimes triple multipart-mime wrapping you need to do to give back the bounced message. A major pain with this standard is that the headers are spread out across the different nested parts, some might be duplicated, and the format is *never* respected by the various MTAs out there. With RFC1893 you have a large number of status codes, but they are combined in weird ways such that you have a primary code with the first digit, a secondary code with the second, and then a "third" code that's the second and third digit combined. Unlike HTTP error codes where there's just a single number mapped to a single status code, in RFC1893 it's this strangely nested first+second+(second+third) thing. Adding to this is there's a few headers where you can put these status codes, and in various formats. Here's a sample of the *parsed* Diagnostic-Code from a gmail bounce message:
 'Diagnostic-Code': [(u'smtp',
                      u'550-5.1.1',
                      u"The email account that you tried to reach does...")],
The end result is that if you want to determine if a message has bounce (not even hard vs. soft) then you have to go trolling through nested attachments looking for headers and parsing their values on the chance they *might* be important to the bounce. h2. Soft and Hard The general consensus is that if a *primary status code* is greater than 4 then the message is a *hard* bounce, anything else is a soft bounce. Of course there's no way this is an absolute truth, and I'm sure there will end up being various complex things people will want to do with a soft vs. a hard bounce. Since there's no way Lamson could know what you want to base your decisions to honor a soft vs. hard bounce, it simply lets you figure out which type it is, and then analyze the information it's collected to determine the bounce. h2. Lamson's Simplification Lamson already converts the email into a normalized form, so the difficult part of parsing out the bounce information is simply trolling through the parts and finding all these randomly dispersed bounce headers. Then it's a matter of parsing the values into something that can be use for analysis in code. To simplify this, Lamson uses a dict of headers and matching regexes to search for in the email:
BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': 
        re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}
Each of these headers shows up about once, maybe more in the parts of a message that has bounced information, and they don't show up at all or rarely in regular messages. What Lamson does is just walk through each part, finds the matches, parses with the regex, and then produces something like this:
{'Action': [(u'failed',)],
 'Content-Description': [(u'Notification',),
                         (u'Undelivered Message',),
                         (u'Delivery report',)],
 'Diagnostic-Code': [(u'smtp',
                  u'550-5.1.1',
                  u"The email account that you tried to reach does...")],
 'Final-Recipient': [(u'rfc822',
              u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')],
 'Received': [(u'by mail.zedshaw.com ...',)],
 'Remote-Mta': [(u'dns', u'gmail-smtp-in.l.google.com')],
 'Reporting-Mta': [(u'dns', u'mail.zedshaw.com')],
 'Status': [(u'5', u'1', u'1')]}
I've abbreviated some of the messages since they get stupidly long, but this shows you the information you get now. Lamson also provides a nice API through the lamson.bounce.BounceAnalyzer class that you'll see in a moment. bq. If you know of additional headers and formats that show up in bounces, let me know. h2. Bounce Probability Now, we've collected up all the different headers you might find in a bounce package, and we've attempted to parse the values into something meaningful. That means, the more of these headers that we find and successfully parse, the more likely the message is a bounce. Lamson keeps a score while it's finding the above headers, and for each one it finds and parses it adds a "point". It then produces a probability <= 1.0 that the message is a bounce or not. The more headers it finds, the closer to 1.0, the higher the chance. So far it looks like any message above 0.3 is most likely a bounce message, with only a few spams that are below that. Checking for a bounce then looks like this:
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
Which is from the test suite, and shows you various ways to get information about the probability of the message being a bounce. h2. Bounce Header Information Finally, you'll need to get at this parsed information to make any final decisions about the bounce. You might want to keep track of which MTAs consistently fail and include them in your black lists. You might want to tune your retries for different kind of soft bounces. Whatever you need to do, you have the original raw headers that Lamson found, but you also have a higher level API you can use:
if msg.is_bounce():
    print "-------"
    print "hard", msg.bounce.is_hard()
    print "soft", msg.bounce.is_soft()
    print 'score', msg.bounce.score
    print 'primary', msg.bounce.primary_status
    print 'secondary', msg.bounce.secondary_status
    print 'combined', msg.bounce.combined_status
    print 'remote_mta', msg.bounce.remote_mta
    print 'reporting_mta', msg.bounce.reporting_mta
    print 'final_recipient', msg.bounce.final_recipient
    print 'diagnostic_codes', msg.bounce.diagnostic_codes
    print 'action', msg.bounce.action
This is a simple sample that prints out the various bits of information available on a bounce message. Here's a sample of the output from the above:
hard True
soft False
score 1.0
primary (5, u'Permanent Failure')
secondary (1, u'Addressing Status')
combined (11, u'Bad destination mailbox address')
remote_mta gmail-smtp-in.l.google.com
reporting_mta mail.zedshaw.com
final_recipient asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
diagnostic_codes (u'550-5.1.1', u"The email account that you tried...")
action failed
This demonstrates how I've pulled out all of the error codes mentioned in RFC1893 and put them into Lamson so that you can get meaningful error messages for the various multi-level error codes you'll encounter. All of these error codes are available in the lamson.bounce module. h2. Comments Or Suggestions That's it for the review of Lamson's bounce detection. It's in the early stages but already showing a lot of promise. It turned out to be much easier than I anticipated, but only after doing the work in lamson.encoding to clean up emails. If you can think of situations that should be covered, and if you have samples of horrible bounce messages, then I'd love to have them. h2. Mailing Lists Coming Back Tomorrow I'll be using this to reimplement the mailing list sample and put it back into the bzr repository, then back online. I'll have a nice status update about that tomorrow as I finish it up. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-14.html0000644000076500000240000001177511230770144023502 0ustar zedshawstaff LamsonProject: Lamson 0.9.6 Sometime Today

Lamson 0.9.6 Sometime Today

I think I’m at a point where bounce detection and handling works really well and is ready for release. I’ve got it running on the latest Lamson demo librelist.com and it works. I’ll be pushing out a new release of Libre List in a little while that does some advanced bounce handling, and if that all works, then I’ll push out 0.9.6.

This release will include bounce handling, the ability to change how state is stored and maintained, the ability to route on the envelope rather than the headers, and the full code to the librelist.com site including Django Integration which really isn’t “integration”.

The plans for the 0.9.7 release are to remove all dependencies on SQLAlchemy to make it clear that you don’t have to use it, and any bug fixes. After that release, assuming there’s nothing major I need to add, I’ll be doing the 1.0 pre-releases.

Shoot me email if you have questions, or hop on #lamson on irc.freenode.org to chat.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-14.txt0000644000076500000240000000200611227105220023331 0ustar zedshawstaffTitle: Lamson 0.9.6 Sometime Today I think I'm at a point where bounce detection and handling works really well and is ready for release. I've got it running on the latest Lamson demo "librelist.com":http://librelist.com/ and it works. I'll be pushing out a new release of Libre List in a little while that does some advanced bounce handling, and if that all works, then I'll push out 0.9.6. This release will include bounce handling, the ability to change how state is stored and maintained, the ability to route on the envelope rather than the headers, and the full code to the librelist.com site including "Django Integration":http://dpaste.de/3qGB/ which really isn't "integration". The plans for the 0.9.7 release are to remove all dependencies on SQLAlchemy to make it clear that you don't have to use it, and any bug fixes. After that release, assuming there's nothing major I need to add, I'll be doing the 1.0 pre-releases. Shoot me email if you have questions, or hop on #lamson on irc.freenode.org to chat. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-19.html0000644000076500000240000001475211230770144023505 0ustar zedshawstaff LamsonProject: I Blame Bounces For The Delay

I Blame Bounces For The Delay

I was going to release Lamson 0.9.6 much earlier than this, but as I worked on the bounce detection feature, I realized that if I just wrote that and a few other cleanups and features, I’d actually have a 1.0pre1 candidate on my hands instead of a 0.9.6 release.

Instead of releasing 0.9.6, I continued to work on librelist.com and got the software smart as hell. While working on librelist.com I managed to improve Lamson to the point where I’ll be comfortable releasing a 1.0pre1 tomorrow.

What’s librelist.com?

If you go librelist.com you can read about it, although the site is still rough and being written. It actually does function however, including basic archives.

For people too lazy to click on the link: librelist.com is just a free mailing list service for open source projects, similar to freenode.org with IRC. The primary purpose is to give open source projects a mailing list they can use that has:

  • The code available (it’s in the Lamson source).
  • No logins, signups, bundled services, captchas, tracking cookies, or other crap getting in your way.
  • No ads on your emails, ever.
  • Full access to your archives using rsync.
  • Easy and smart signup process.
  • Spam blocking and bounce protection galore (although spam is off right now).

Try The Lists

I’ll have an announcement tomorrow laying out the big things you’ll get, but for now if you want to track Lamson and participate, as well as test the librelist.com code, then try subscribing to lamson@librelist.com and saying hello.

If you’re interested in librelist.com development, then subscribe to meta@librelist.com and I’ll be sending out announcements related to librelist there.

The Lamson 1.0pre1 Plan

I haven’t needed to touch Lamson much over the last few days, apart from some surgery on the Python Maildir class to add a safety feature. The code currently in bzr is also very stable and has been sped up a bit. I’m looking to basically spend tomorrow auditing the code and writing docs with the goal of pushing out a release in the evening.

The goal then will be to make it solid and not add many more features so that I can get a 1.0 release out for real by the end of July.

See you tomorrow!


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-19.txt0000644000076500000240000000446211230540446023355 0ustar zedshawstaffTitle: I Blame Bounces For The Delay I was going to release Lamson 0.9.6 much earlier than this, but as I worked on the bounce detection feature, I realized that if I just wrote that and a few other cleanups and features, I'd actually have a *1.0pre1* candidate on my hands instead of a 0.9.6 release. Instead of releasing 0.9.6, I continued to work on "librelist.com":http://librelist.com/ and got the software smart as hell. While working on librelist.com I managed to improve Lamson to the point where I'll be comfortable releasing a 1.0pre1 tomorrow. h2. What's librelist.com? If you go "librelist.com":http://librelist.com/ you can read about it, although the site is still rough and being written. It actually does function however, including basic archives. For people too lazy to click on the link: librelist.com is just a free mailing list service for open source projects, similar to freenode.org with IRC. The primary purpose is to give open source projects a mailing list they can use that has: * The code available (it's in the Lamson source). * No logins, signups, bundled services, captchas, tracking cookies, or other crap getting in your way. * No ads on your emails, ever. * Full access to your archives using rsync. * Easy and smart signup process. * Spam blocking and bounce protection galore (although spam is off right now). h2. Try The Lists I'll have an announcement tomorrow laying out the big things you'll get, but for now if you want to track Lamson and participate, as well as test the librelist.com code, then try subscribing to "lamson@librelist.com":mailto:lamson@librelist.com and saying hello. If you're interested in librelist.com development, then subscribe to "meta@librelist.com":mailto:meta@librelist.com and I'll be sending out announcements related to librelist there. h2. The Lamson 1.0pre1 Plan I haven't needed to touch Lamson much over the last few days, apart from some surgery on the Python Maildir class to add a safety feature. The code currently in bzr is also very stable and has been sped up a bit. I'm looking to basically spend tomorrow auditing the code and writing docs with the goal of pushing out a release in the evening. The goal then will be to make it solid and not add many more features so that I can get a 1.0 release out for real by the end of July. See you tomorrow! lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-20.html0000644000076500000240000002047611230770144023475 0ustar zedshawstaff LamsonProject: Lamson 1.0pre1 Released

Lamson 1.0pre1 Released

Tonight I’m releasing Lamson 1.0pre1 with all the latest improvements I’ve made while making librelist.com and taking feedback from a few people using Lamson. The goal from now on will be to basically squash bugs and write docs, with only a rare feature or two as I find them needed on projects.

What’s In This Release

This release features really only four major enhancements, and a bunch of little fixes here and there.

  1. There’s now a working mailing list example in examples/librelist.
  2. The router now uses the envelope header To (message.To) instead of the headers.
  3. There is now very solid bounce detection and analysis.
  4. Modules can specify anything as the state key (from, to+from, module+to, etc.)

Here’s the important changes from the change log by category:

Enhancements

  • 340 Added a version command finally, with some good information.
  • 330 There is now a safe=False option to lamson.queue.Queue to use a modified Maildir that md5 hashes the original hostname used.
  • 321 Routing it more deterministic and a bit quicker. It now uses the envelope To for routing.
  • 320 Refactored the routing to it is more deterministic and doesn’t loop over the functions mulitple times. Still some work to do though.
  • 314 Bounce information now includes the original messages in easier to access forms.
  • 308 Got bounce handling working and tested.
  • 303 Slight reorg with the way @state_key_generator is implemented.
  • 302 Implemented a means to specify that a module will key off of different parts of a message.
  • 301 Got tired of not seeing errors during test runs so I made an option in the Router to explode on exceptions vs. log them.
  • 300 Bounce routing decorator for doing the actual routing of messages that are bounces.
  • 296 Implemented a bounce analysis method that is simple but seems to work on most bounce mail.

Bug Fixes

  • 329 Found an email that had a nasty header causing an edge case in the parser.
  • 325 Bounce detection was using the wrong email address.
  • 319 Removed the egg dependency on sqlalchemy.
  • 318 Moved the bounce stuff around in order to make it more advanced.
  • 307 Fixed up the @bounce_to decorator so that it returns the next state.
  • 294 Small bug in multipart handling where content types of 'message/’ can be multipart too.

Librelist Related

  • 323 librelist.com sample is looking good, bounce handling is working, archives are started.
  • 322 Super bounce handling for librelist.com.
  • 306 Got the first cut of the whole app.handlers.admin working, including bounces, but Lamson bounce detection has a flaw.
  • 305 Got the basics of making mailing lists, managing subscribers.
  • 304 Initial commit of the librelist.com sample application.

Also, librelist demonstrates how to integrate Lamson with Django which turns out to just require a couple lines of code. I’ll have some documentation on that soon.

Docs and Website Related

  • 317 Removed references to SQLAlchemy being needed in the docs.

Getting This Release

As usual, try to install it using easy_install and make sure you do the upgrade:

$ sudo easy_install -U lamson

This release does not depend on SQLAlchemy anymore, since you can really use Lamson with any data source you can access with Python. As mentioned above, the librelist sample uses Django models.

If you want to download the source you can visit the releases page and grab it from there.

Mailing List Back Online

Finally, the lamson mailing list is now back online, so you can subscribe to it and get help. Read the lists page to find out more.

IRC Channel

I’m also more active in the IRC channel #lamson on irc.freenode.org. Come by if you need help.

Thanks again folks, and shoot me any bugs you find.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-07-20.txt0000644000076500000240000000754611230764565023365 0ustar zedshawstaffTitle: Lamson 1.0pre1 Released Tonight I'm releasing Lamson 1.0pre1 with all the latest improvements I've made while making "librelist.com":http://librelist.com/ and taking feedback from a few people using Lamson. The goal from now on will be to basically squash bugs and write docs, with only a rare feature or two as I find them needed on projects. h2. What's In This Release This release features really only four major enhancements, and a bunch of little fixes here and there. # There's now a working mailing list example in examples/librelist. # The router now uses the envelope header To (message.To) instead of the headers. # There is now very solid bounce detection and analysis. # Modules can specify anything as the state key (from, to+from, module+to, etc.) Here's the important changes from the change log by category: h3. Enhancements * 340 Added a version command finally, with some good information. * 330 There is now a safe=False option to lamson.queue.Queue to use a modified Maildir that md5 hashes the original hostname used. * 321 Routing it more deterministic and a bit quicker. It now uses the envelope To for routing. * 320 Refactored the routing to it is more deterministic and doesn't loop over the functions mulitple times. Still some work to do though. * 314 Bounce information now includes the original messages in easier to access forms. * 308 Got bounce handling working and tested. * 303 Slight reorg with the way @state_key_generator is implemented. * 302 Implemented a means to specify that a module will key off of different parts of a message. * 301 Got tired of not seeing errors during test runs so I made an option in the Router to explode on exceptions vs. log them. * 300 Bounce routing decorator for doing the actual routing of messages that are bounces. * 296 Implemented a bounce analysis method that is simple but seems to work on most bounce mail. h3. Bug Fixes * 329 Found an email that had a nasty header causing an edge case in the parser. * 325 Bounce detection was using the wrong email address. * 319 Removed the egg dependency on sqlalchemy. * 318 Moved the bounce stuff around in order to make it more advanced. * 307 Fixed up the @bounce_to decorator so that it returns the next state. * 294 Small bug in multipart handling where content types of 'message/' can be multipart too. h3. Librelist Related * 323 librelist.com sample is looking good, bounce handling is working, archives are started. * 322 Super bounce handling for librelist.com. * 306 Got the first cut of the whole app.handlers.admin working, including bounces, but Lamson bounce detection has a flaw. * 305 Got the basics of making mailing lists, managing subscribers. * 304 Initial commit of the librelist.com sample application. Also, librelist demonstrates how to integrate Lamson with "Django":http://www.djangoproject.com/ which turns out to just require a couple lines of code. I'll have some documentation on that soon. h3. Docs and Website Related * 317 Removed references to SQLAlchemy being needed in the docs. h2. Getting This Release As usual, try to install it using "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall and make sure you do the upgrade:
$ sudo easy_install -U lamson
This release does not depend on SQLAlchemy anymore, since you can really use Lamson with any data source you can access with Python. As mentioned above, the librelist sample uses Django models. If you want to download the source you can visit the "releases":/releases/ page and grab it from there. h2. Mailing List Back Online Finally, the lamson mailing list is now back online, so you can subscribe to it and get help. Read "the lists page":/lists/ to find out more. h2. IRC Channel I'm also more active in the IRC channel #lamson on irc.freenode.org. Come by if you need help. Thanks again folks, and shoot me any bugs you find. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-08-03.html0000644000076500000240000005221611235577731023510 0ustar zedshawstaff LamsonProject: Lamson 1.0pre2, HTML Email, Standalone

Lamson 1.0pre2, HTML Email, Standalone

Lamson 1.0pre2 features two features that might signal the end of the beginning or the beginning of the end, depending on your perspective: HTML Email and Lamson Standalone. HTML Email support comes from a new module lamson.html that gives a nice template method to send out HTML to victims…uh…customers. Lamson Standalone will be a way to run Lamson as your customized email server instead of another server like Postfix.

Both of these features will be done for Lamson 1.0, but are currently just getting started as of 1.0pre2. Once they’re done that will be the last two requested features people had before 1.0.

Getting This Release

As usual, you can get this release from /releases or if you are too lazy to read that page then do this:

$ sudo easy_install -U lamson

In addition, if you want play with the new features you’ll need to install these (optional) Python libraries:

$ sudo easy_install -U pydns
$ sudo easy_install -U beautifulsoup
$ sudo easy_install -U markdown2
$ sudo easy_install -U clevercss

I’ll decide if I should make these mandatory or optional with the next release based on people’s feedback.

Lamson Standalone

Quite a few people have asked for a way to install Lamson as their primary email server. Usually they want it for just their own email on a machine just for them where installing something like Postfix is overkill or just too hard. This is entirely possible with Lamson, but it’s kind of not the right use for it since simple delivery of mail is much better handled and implemented by an older server like Postfix.

Rather than resist giving people a way to setup Lamson as their primary mail server, I’ve decided to start making it possible for them. By the next release (1.0pre3) you’ll be able to run Lamson as your only email server for small installations handling mail for one person.

I’ll also include a screen cast showing people how to use the standalone functionality to build a Personal Mail Management Server to filter and control their mail without any other mail server. I’m going to treat this as a first project for most people wanting to get started using Lamson for their email.

As of 1.0pre2, the functionality that’s available is code in the lamson.server.Relay that will use PyDNS to query up the MX host for recipient addresses. It’s fairly primitive right now, but you use it by creating a Relay with the host explicitly None:

relay = server.Relay(host=None, port=25, debug=1)

Once you do this the Relay will lookup the hosts rather than trying to send through a relay host.

Don’t go crazy with this yet, since it has to be tested with various kind of nasty email addressing out there, and it needs to have a way to generate a bounce when it has an error.

HTML Email

Well, I broke down and implemented my idea for making HTML Email easy as hell to generate. In the past I didn’t want to include simple HTML support because, well, HTML Email is annoying as hell and I didn’t want to deal with the support headaches. I knew once I threw HTML generation into Lamson I’d have an army of marketing people using Lamson poorly to generate their marketing materials. I also think that HTML formatting in email doesn’t work as a customer development strategy.

Yet, every time I tell someone about Lamson, the very first, second, third, and 300th thing they ask is if it does HTML Email. Over and over and over this was the most important feature, above spam blocking, filtering, building applications, intelligent state management, or anything else that Lamson supports.

Well, if the people want HTML email generation, then the people will get it. I introduce you to lamson.html which makes it trivial to produce HTML in your email and doing it in a nice clean way using CleverCSS and Jinja2 templates.

Let’s start with the simplest little example that will send out a disgusting html template:

import sys
from lamson import html, server
from config import testing

relay = server.Relay(host=None, port=25, debug=1)
hs = html.HtmlMail("style.css", "html_test.html")

title = "Test Message HTML"

msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")

relay.deliver(msg)

You could run this right out of the Lamson source tree like this:

export PYTHONPATH=tests
python sender.py thedude@thedude.com victim@gmail.com

The result would look like this in victim’s email:

How lamson.html.HtmlMail Works

The rationale behind the HtmlMail class is that you’ll typically have a template you want to send, some CSS, and then body content that you’ll plug into the template as a slug for each person. What HtmlMail does is let you setup the template as HTML with Jinja2, and then specify your CSS using CleverCSS (which is way easier).

When you go to send, you generate a Markdown template as the body. This lets you write the actual body of your HTML mail the same way you would a regular email, but still let’s you get a good HTML output using the HTML/CSS templates. With markdown regular folks can write the marketing copy for your spam while keeping the design separated.

HtmlMail then glues this all together by doing the following:

  1. Parses your CleverCSS template (after running it through Jinja2).
  2. Runs your content markdown through Jinja2 and markdown2 just like all your other Lamson templates.
  3. Injects your CSS into your HTML tags so that you get your styles even though many clients rip out the style tags from your HTML. See the output sample below.
  4. Knits your generated content into your HTML template as the {{content}} variable.

The end result is that these two lines of code from the above sample:

...
hs = html.HtmlMail("style.css", "html_test.html")

...
msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")

Will let you blast out well formatted HTML emails that have a high probability of displaying in most mail clients.

How The HTML And CSS Looks

Here’s what each of the files in the above sample look like. First the style.css:

body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h2:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black

Notice that CleverCSS supports quite a few very cool features, like calling functions, nesting, variables, and calculations.

Next you have the outer template html_template.html that wraps your content markdown template. Notice that there’s no style tag where the above CleverCSS is placed. That gets done later by lamson.html.HtmlMail.

<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h2 id="dull">All done.</h2>
    </body>
</html>

Finally, you have the content.markdown file that has your basic markdown formatted email:

Hello
=====

I would *love* for you to tell me what is going on here joe.  NOW!

Alright
-------

This is the best I can come up with.

Zed

You can use any format you want by changing a setting when you construct your HtmlMail object, but markdown seems to be the closest to what people would send as plain text for their email. In fact, I’d like to find a way to send the raw markdown as the plaintext version of each HTML email that goes out.

With the above three components, you then get the following output:

<html>
<head>
<title>Test Message HTML</title>
</head>
<body style="margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="background: black; color: white">Test Message HTML</h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h2 id="dull" style="background: gray; color: black">All done.</h2>
</body>
</html>

Like magic HtmlMail has taken your CleverCSS file and merged it into the tags of your HTML to style it, making it work in most clients without forcing you to do it manually. It does this by walking the CSS and generated HTML with BeautifulSoup and setting the style attribute as it goes.

Librelist JSON Archives

There’s been a ton of work on librelist.com to make it work better, but the big feature is a complete JSON dump of the mail archives and a new Archive Browser for reading the archives.

The browser is just a fast one day hack I did to prove that the JSON archive format was good. It uses JQuery UI to build the interface, and has no servers side software other than Nginx. Yes, you read that right, there is no Django behind this, just a pure Nginx setup serving files and directory indexes.

The real meat of this setup is the Lamson server on librelist and an Nginx module.

First, the librelist server generates a JSON version of each archived email which you can see here with your browser. This JSON actually loads as an object in JavaScript, so you have code like this:

function appendMessage(msg) {
    display = '<div id="message"><div id="header">' +
        '<div>' + msg.headers['Date'] + '</div>' +
        '<div id="addressing">' + msg.headers['From'] + '</div>' +
        '<div id="subject">' + msg.headers['Subject'] + '</div>' +
        '</div><div id="body"><pre>' +
        summarize(msg) +
        '</pre></div></div>'

        $("#messages").append(display);
}

Second, there’s a tiny modification to Nginx’s autoindex I made so that it will generate a JSON version of the index instead of HTML so that you can “browse” the stored JSON files via JavaScript:

function loadDay(date) {
    if(window.MAILING_LIST) {
        $.getJSON("/archives/" + window.MAILING_LIST + date + "/json/", {},
        function(data) {
            $("#messages").empty();
            base = data[0]
            msgs = data[1]
            for(i in msgs) {
                fetchMessage(base + msgs[i])
            }
        });
    } else {
        $("#messages").html("<h2>Now pick a list to look at for that day.</h2>");
    }
}

With those two components I can export a JSON version of the mailing list archives without having to run a large web framework.

ChangeLog

For those who are interested, here’s the Bazaar logs from this release, separated into changes to Lamson and changes to the included librelist.com sample.

Lamson Changes

  • Cleanup and tuning of the html code.
  • Documenting the HtmlMail API.
  • New HTML email generation with CleverCSS support and merging the CSS into the HTML results.
  • Additional tests for the new MX query style of Relay.
  • Implemented a feature to let the relay do its own delivery, rather than needing a smart host. Requires PyDNS.
  • Cleaned up the build so that it removes junk from the examples.
  • Fixed up the lamson default help command so that it is more succinct.
  • Properly skips spam filtering if the spam databse doesn’t exist.
  • Made the spam filter not barf if there’s no spam db, which helps when testing.
  • Now don’t bother storing the START state since it’s the default.
  • Cleaned up mail some more and deprecated the MailRequest.msg and MailResponse.msg to be replaced with .base (since that’s what it is).
  • Added a simple method to copy the attachments of a MailRequest to a MailResponse.
  • Got decent attachment copying implemented, mail api needs a bit of cleanup.
  • Return an empty string when trying to encode a none.
  • Minor bug in the safequeue, added logging so you can track oversize.
  • Getting the headers right for replies to work, removed the first message.
  • Added options to Queue and QueueReceiver to limit the size of incoming mail.
  • Added a version command finally, with some good information.

librelist.com Changes

  • Force the permissions to be correct for serving up the archives.
  • Make sure that bounces are always saved for later.
  • Json convert script can’t go there.
  • Need to install simplejson to make the json work on librelist.
  • Prevent loop back of messages from lamson to itself, and stop messages that should not be considered lists like unbounce and noreply.
  • Implements a json version of the mailing list archives in addition to the regular MIME encoded ones.
  • Update of changes from the live server for setting up the spam system.
  • Migration to install the spam database on deployment.
  • Added spam filtering to the admin handler where it counts.
  • Confirmations are removed after they are verified.
  • Template for bad list name help.
  • Tweaked the formatting for the bad list name helper, and made sure that it returns to START.
  • Now giving out an error message for people who get the name format wrong.
  • Added additional headers needed for achives, and try to maintain the date and message-id headers.
  • Production and staging deployment scripts setup and a migration to make those work.
  • Implemented a simple bash based deployment setup for automating librelist deploys and migrations.
  • Nope, can’t add the – yet without some regex wizardry.
  • Small change to allow – in the mailing list names.
  • Updated the messages librelist sends out for readability.

lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-08-03.txt0000644000076500000240000003551011235577723023362 0ustar zedshawstaffTitle: Lamson 1.0pre2, HTML Email, Standalone Lamson 1.0pre2 features two features that might signal the end of the beginning or the beginning of the end, depending on your perspective: HTML Email and Lamson Standalone. HTML Email support comes from a new module "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html that gives a nice template method to send out HTML to victims...uh...customers. Lamson Standalone will be a way to run Lamson as your customized email server instead of another server like Postfix. Both of these features will be done for Lamson 1.0, but are currently just getting started as of 1.0pre2. Once they're done that will be the last two requested features people had before 1.0. h2. Getting This Release As usual, you can get this "release from /releases":/releases/ or if you are too lazy to read that page then do this:
$ sudo easy_install -U lamson
In addition, if you want play with the new features you'll need to install these (optional) Python libraries:
$ sudo easy_install -U pydns
$ sudo easy_install -U beautifulsoup
$ sudo easy_install -U markdown2
$ sudo easy_install -U clevercss
I'll decide if I should make these mandatory or optional with the next release based on people's feedback. h2. Lamson Standalone Quite a few people have asked for a way to install Lamson as their primary email server. Usually they want it for just their own email on a machine just for them where installing something like Postfix is overkill or just too hard. This is entirely possible with Lamson, but it's kind of not the right use for it since simple delivery of mail is much better handled and implemented by an older server like Postfix. Rather than resist giving people a way to setup Lamson as their primary mail server, I've decided to start making it possible for them. By the next release (1.0pre3) you'll be able to run Lamson as your only email server for small installations handling mail for one person. I'll also include a "screen cast":/videos/ showing people how to use the standalone functionality to build a *Personal Mail Management Server* to filter and control their mail without any other mail server. I'm going to treat this as a first project for most people wanting to get started using Lamson for their email. As of 1.0pre2, the functionality that's available is code in the "lamson.server.Relay":http://lamsonproject.org/docs/api/lamson.server.Relay-class.html that will use "PyDNS":http://pydns.sourceforge.net/ to query up the MX host for recipient addresses. It's fairly primitive right now, but you use it by creating a Relay with the host explicitly None:
relay = server.Relay(host=None, port=25, debug=1)
Once you do this the Relay will lookup the hosts rather than trying to send through a relay host. Don't go crazy with this yet, since it has to be tested with various kind of nasty email addressing out there, and it needs to have a way to generate a bounce when it has an error. h2. HTML Email Well, I broke down and implemented my idea for making HTML Email easy as hell to generate. In the past I didn't want to include simple HTML support because, well, HTML Email is annoying as hell and I didn't want to deal with the support headaches. I knew once I threw HTML generation into Lamson I'd have an army of marketing people using Lamson poorly to generate their marketing materials. I also think that HTML formatting in email doesn't work as a customer development strategy. Yet, every time I tell someone about Lamson, the very first, second, third, and 300th thing they ask is if it does HTML Email. Over and over and over this was the most important feature, above spam blocking, filtering, building applications, intelligent state management, or anything else that Lamson supports. Well, if the people want HTML email generation, then the people will get it. I introduce you to "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html which makes it trivial to produce HTML in your email and doing it in a nice clean way using "CleverCSS":http://sandbox.pocoo.org/clevercss/ and "Jinja2":http://jinja.pocoo.org/2/ templates. Let's start with the simplest little example that will send out a disgusting html template:
import sys
from lamson import html, server
from config import testing

relay = server.Relay(host=None, port=25, debug=1)
hs = html.HtmlMail("style.css", "html_test.html")

title = "Test Message HTML"

msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")

relay.deliver(msg)
You could run this right out of the Lamson source tree like this:
export PYTHONPATH=tests
python sender.py thedude@thedude.com victim@gmail.com
The result would look like this in victim's email: !html_email_in_gmail.png! h2. How lamson.html.HtmlMail Works The rationale behind the HtmlMail class is that you'll typically have a template you want to send, some CSS, and then body content that you'll plug into the template as a slug for each person. What HtmlMail does is let you setup the template as HTML with Jinja2, and then specify your CSS using CleverCSS (which is way easier). When you go to send, you generate a "Markdown":http://daringfireball.net/projects/markdown/ template as the body. This lets you write the actual body of your HTML mail the same way you would a regular email, but still let's you get a good HTML output using the HTML/CSS templates. With markdown regular folks can write the marketing copy for your spam while keeping the design separated. HtmlMail then glues this all together by doing the following: # Parses your CleverCSS template (after running it through Jinja2). # Runs your content markdown through Jinja2 and "markdown2":http://code.google.com/p/python-markdown2/ just like all your other Lamson templates. # Injects your CSS *into* your HTML tags so that you get your styles even though many clients rip out the *style* tags from your HTML. See the output sample below. # Knits your generated content into your HTML template as the {{content}} variable. The end result is that these two lines of code from the above sample:
...
hs = html.HtmlMail("style.css", "html_test.html")

...
msg = hs.respond(locals(), "content.markdown", From=sys.argv[1], 
                 To=sys.argv[2], Subject="Test %(title)s")
Will let you blast out well formatted HTML emails that have a high probability of displaying in most mail clients. h2. How The HTML And CSS Looks Here's what each of the files in the above sample look like. First the @style.css@:
body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h2:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black
Notice that "CleverCSS":http://sandbox.pocoo.org/clevercss/ supports quite a few very cool features, like calling functions, nesting, variables, and calculations. Next you have the outer template @html_template.html@ that wraps your content markdown template. Notice that there's no *style* tag where the above CleverCSS is placed. That gets done later by lamson.html.HtmlMail.
<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h2 id="dull">All done.</h2>
    </body>
</html>
Finally, you have the @content.markdown@ file that has your basic markdown formatted email:
Hello
=====


I would *love* for you to tell me what is going on here joe.  NOW!


Alright
-------


This is the best I can come up with.

Zed
You can use any format you want by changing a setting when you construct your HtmlMail object, but markdown seems to be the closest to what people would send as plain text for their email. In fact, I'd like to find a way to send the raw markdown as the plaintext version of each HTML email that goes out. With the above three components, you then get the following output:
<html>
<head>
<title>Test Message HTML</title>
</head>
<body style="margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="background: black; color: white">Test Message HTML</h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h2 id="dull" style="background: gray; color: black">All done.</h2>
</body>
</html>
Like magic HtmlMail has taken your CleverCSS file and merged it into the tags of your HTML to style it, making it work in most clients without forcing you to do it manually. It does this by walking the CSS and generated HTML with "BeautifulSoup":http://www.crummy.com/software/BeautifulSoup/ and setting the *style* attribute as it goes. h2. Librelist JSON Archives There's been a ton of work on "librelist.com":http://librelist.com/ to make it work better, but the big feature is a complete JSON dump of the mail archives and a new "Archive Browser":http://librelist.com/browser/ for reading the archives. The browser is just a fast one day hack I did to prove that the JSON archive format was good. It uses "JQuery UI":http://jqueryui.com/ to build the interface, and has *no servers side software other than Nginx.* Yes, you read that right, there is no Django behind this, just a pure "Nginx":http://nginx.net/ setup serving files and directory indexes. The real meat of this setup is the Lamson server on librelist and an Nginx module. First, the librelist server generates a JSON version of each archived email which you "can see here":http://librelist.com/archives/lamson/2009/07/30/json/1248947932.M724307P15430Q6.09c5769d5b9f3d575cefc2ccb51877ec.json with your browser. This JSON actually loads as an object in JavaScript, so you have code like this:
function appendMessage(msg) {
    display = '<div id="message"><div id="header">' +
        '<div>' + msg.headers['Date'] + '</div>' +
        '<div id="addressing">' + msg.headers['From'] + '</div>' +
        '<div id="subject">' + msg.headers['Subject'] + '</div>' +
        '</div><div id="body"><pre>' +
        summarize(msg) +
        '</pre></div></div>'
        
        $("#messages").append(display);
}
Second, there's a tiny modification to Nginx's "autoindex":http://wiki.nginx.org/NginxHttpAutoindexModule I made so that it will generate a "JSON version of the index instead of HTML":http://librelist.com/archives/lamson/2009/07/ so that you can "browse" the stored JSON files via JavaScript:
function loadDay(date) {
    if(window.MAILING_LIST) {
        $.getJSON("/archives/" + window.MAILING_LIST + date + "/json/", {},
        function(data) {
            $("#messages").empty();
            base = data[0]
            msgs = data[1]
            for(i in msgs) {
                fetchMessage(base + msgs[i])
            }
        });
    } else {
        $("#messages").html("<h2>Now pick a list to look at for that day.</h2>");
    }
}
With those two components I can export a JSON version of the mailing list archives without having to run a large web framework. h2. ChangeLog For those who are interested, here's the Bazaar logs from this release, separated into changes to Lamson and changes to the included librelist.com sample. h2. Lamson Changes * Cleanup and tuning of the html code. * Documenting the HtmlMail API. * New HTML email generation with CleverCSS support and merging the CSS into the HTML results. * Additional tests for the new MX query style of Relay. * Implemented a feature to let the relay do its own delivery, rather than needing a smart host. Requires PyDNS. * Cleaned up the build so that it removes junk from the examples. * Fixed up the lamson default help command so that it is more succinct. * Properly skips spam filtering if the spam databse doesn't exist. * Made the spam filter not barf if there's no spam db, which helps when testing. * Now don't bother storing the START state since it's the default. * Cleaned up mail some more and deprecated the MailRequest.msg and MailResponse.msg to be replaced with .base (since that's what it is). * Added a simple method to copy the attachments of a MailRequest to a MailResponse. * Got decent attachment copying implemented, mail api needs a bit of cleanup. * Return an empty string when trying to encode a none. * Minor bug in the safequeue, added logging so you can track oversize. * Getting the headers right for replies to work, removed the first message. * Added options to Queue and QueueReceiver to limit the size of incoming mail. * Added a version command finally, with some good information. h2. librelist.com Changes * Force the permissions to be correct for serving up the archives. * Make sure that bounces are always saved for later. * Json convert script can't go there. * Need to install simplejson to make the json work on librelist. * Prevent loop back of messages from lamson to itself, and stop messages that should not be considered lists like unbounce and noreply. * Implements a json version of the mailing list archives in addition to the regular MIME encoded ones. * Update of changes from the live server for setting up the spam system. * Migration to install the spam database on deployment. * Added spam filtering to the admin handler where it counts. * Confirmations are removed after they are verified. * Template for bad list name help. * Tweaked the formatting for the bad list name helper, and made sure that it returns to START. * Now giving out an error message for people who get the name format wrong. * Added additional headers needed for achives, and try to maintain the date and message-id headers. * Production and staging deployment scripts setup and a migration to make those work. * Implemented a simple bash based deployment setup for automating librelist deploys and migrations. * Nope, can't add the - yet without some regex wizardry. * Small change to allow - in the mailing list names. * Updated the messages librelist sends out for readability. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-08-22.html0000644000076500000240000002345011243717167023505 0ustar zedshawstaff LamsonProject: Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage

Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage

I happy to announce probably one of the last few releases before I officially put the 1.0 stamp on Lamson. This last 1% of the things I want to do takes a while, but it really puts a good shine on the project.

What I’ve got in 1.0pre4 is tons of docs and 100% test coverage. Most of the features that were added are nice-to-haves that I’ve found useful while developing my various sites.

Getting The Release

As usual, sudo easy_install lamson is your friend. If you want to use the HTML generation features, then you’ll want to also install:

BeautifulSoup
CleverCSS
markdown2

For completeness, here’s the remaining packages I have installed in most of the virtualenvs for my applications:

ipython
mock
nose
Jinja2
lockfile
pydns
spambayes
chardet
lxml
python_daemon
pytyrant

You can also grab source releases and download instructions from this site if PyPI is failing you.

100% Test Coverage

First, I’ve managed to get the test coverage for Lamson up to 100%:

Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               124    124   100%   
lamson.bounce              91     91   100%   
lamson.commands           176    176   100%   
lamson.confirm             57     57   100%   
lamson.encoding           238    238   100%   
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.html                62     62   100%   
lamson.mail               140    140   100%   
lamson.queue               74     74   100%   
lamson.routing            218    218   100%   
lamson.server              80     80   100%   
lamson.spam                80     80   100%   
lamson.utils               56     56   100%   
lamson.version              1      1   100%   
lamson.view                22     22   100%   
-----------------------------------------------------
TOTAL                    1429   1429   100%   
-----------------------------------------------------
Ran 156 tests in 16.305s

It’s mostly a vanity thing, but doing this did uncover a couple of minor little bugs here and there, and it makes people feel better trusting Lamson.

Docs, Docs, Docs

I firmly believe that a good software project has both API style documentation and guided documentation. That’s why I spent almost this whole time working on documenting all the parts of Lamson people need and making sure the generated docs were complete.

What I’ve done is reorganized the documentation section so that you can find topic by their categories, and then documented most of the features people need to use daily. Here’s some highlights:

I especially like how the HTML Email generation came out as a feature, but I really like the Bounce Detection documentation the most.

New Confirmation API

This release has a few little bug fixes, but mostly it has a solidified feature for doing Confirmations easily.

HTML/Text Dual Email

Previously the HTML Email Generation feature could only generate HTML email reliably, but I managed to track down a bug that lets you now craft dual email. I even went a step further and made it easy to simply use your markdown content templates as your text/plain alternative when you send out HTML Email.

Multiple Recipient Routing

There was no way to avoid this, so I just implemented it. Now when a message is for multiple recipients (in the envelope) Lamson will route the message to each address. That means if it is destined for multiple people, then Lamson will effectively try to match each of those people and run them through your handlers separately.

I was worried this would cause problems, but so far it’s working fine. If you see that it can be used as a DOS attack, then I’ll work on a way to limit the number of recipients that Lamson will allow (similar to how you can restrict the size of messages off a Queue).

Test And Report

As usual, please test this out, review the documentation, and report back to me any problems you find.

I’ll be in #lamson on irc.freenode.org more, and will probably install a IRC bot to notify when I post new code to the repository.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-08-22.txt0000644000076500000240000001221711243717140023346 0ustar zedshawstaffTitle: Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage I happy to announce probably one of the last few releases before I officially put the 1.0 stamp on Lamson. This last 1% of the things I want to do takes a while, but it really puts a good shine on the project. What I've got in 1.0pre4 is "tons of docs":http://lamsonproject.org/docs/ and 100% test coverage. Most of the features that were added are nice-to-haves that I've found useful while developing my various sites. h2. Getting The Release As usual, @sudo easy_install lamson@ is your friend. If you want to use the HTML generation features, then you'll want to also install:
BeautifulSoup
CleverCSS
markdown2
For completeness, here's the remaining packages I have installed in most of the virtualenvs for my applications:
ipython
mock
nose
Jinja2
lockfile
pydns
spambayes
chardet
lxml
python_daemon
pytyrant
You can also grab "source releases":/releases/ and "download instructions":/download.html from this site if PyPI is failing you. h2. 100% Test Coverage First, I've managed to get the test coverage for Lamson up to 100%:
Name                    Stmts   Exec  Cover   Missing
-----------------------------------------------------
lamson                      0      0   100%   
lamson.args               124    124   100%   
lamson.bounce              91     91   100%   
lamson.commands           176    176   100%   
lamson.confirm             57     57   100%   
lamson.encoding           238    238   100%   
lamson.handlers             0      0   100%   
lamson.handlers.log         4      4   100%   
lamson.handlers.queue       6      6   100%   
lamson.html                62     62   100%   
lamson.mail               140    140   100%   
lamson.queue               74     74   100%   
lamson.routing            218    218   100%   
lamson.server              80     80   100%   
lamson.spam                80     80   100%   
lamson.utils               56     56   100%   
lamson.version              1      1   100%   
lamson.view                22     22   100%   
-----------------------------------------------------
TOTAL                    1429   1429   100%   
-----------------------------------------------------
Ran 156 tests in 16.305s
It's mostly a vanity thing, but doing this did uncover a couple of minor little bugs here and there, and it makes people feel better trusting Lamson. h2. Docs, Docs, Docs I firmly believe that a good software project has *both* "API":http://lamsonproject.org/docs/api/lamson-module.html style documentation and guided documentation. That's why I spent almost this whole time working on documenting all the parts of Lamson people need and making sure the generated docs were complete. What I've done is reorganized the documentation section so that you can find topic by their categories, and then documented most of the features people need to use daily. Here's some highlights: * "Unit Testing":http://lamsonproject.org/docs/unit_testing.html * "Confirmations":http://lamsonproject.org/docs/confirmations.html * "Filtering Spam":http://lamsonproject.org/docs/filtering_spam.html * "Bounce Detection":http://lamsonproject.org/docs/bounce_detection.html * "HTML Email Generation":http://lamsonproject.org/docs/html_email_generation.html * "Unicode Encoding/Decoding":http://lamsonproject.org/docs/unicode_encoding_and_decoding.html * "Hooking Into Django":http://lamsonproject.org/docs/hooking_into_django.html * "Tons more...":http://lamsonproject.org/docs/ I especially like how the HTML Email generation came out as a feature, but I really like the "Bounce Detection":http://lamsonproject.org/docs/bounce_detection.html documentation the most. h2. New Confirmation API This release has a few little bug fixes, but mostly it has a solidified feature for doing "Confirmations":http://lamsonproject.org/docs/confirmations.html easily. h2. HTML/Text Dual Email Previously the "HTML Email Generation":http://lamsonproject.org/docs/html_email_generation.html feature could only generate HTML email reliably, but I managed to track down a bug that lets you now craft dual email. I even went a step further and made it easy to simply use your markdown content templates as your text/plain alternative when you send out HTML Email. h2. Multiple Recipient Routing There was no way to avoid this, so I just implemented it. Now when a message is for multiple recipients (in the envelope) Lamson will route the message to each address. That means if it is destined for multiple people, then Lamson will effectively try to match each of those people and run them through your handlers separately. I was worried this would cause problems, but so far it's working fine. If you see that it can be used as a DOS attack, then I'll work on a way to limit the number of recipients that Lamson will allow (similar to how you can restrict the size of messages off a Queue). h2. Test And Report As usual, please test this out, review the documentation, and report back to me any problems you find. I'll be in #lamson on irc.freenode.org more, and will probably install a IRC bot to notify when I post new code to the repository. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-09-07.html0000644000076500000240000001237411251323150023474 0ustar zedshawstaff LamsonProject: Lamson 1.0pre5 Out

Lamson 1.0pre5 Out

I just pushed Lamson 1.0pre5 to PyPI for your enjoyment:

PyPI Download

You can also get it from releases or by doing the usual easy_install.

This is a small bugfix release that only adds one small feature. If you’re running with the SMTPReceiver, you can raise a server.SMTPError with an error code you want and the SMTPReceiver will return that as the error code to the client.

For example, if you do this in your handler:

@route(".+")
def START(message):
    raise server.SMTPError(550)

Then it will generate a reasonable error message (using lamson.bounce messages) and give that to the client. You can also give a second parameter to SMTPError that will be the error message if you don’t like the automatic ones.

Only warning with this is use it sparingly, as it then ties you always running Lamson as an SMTPReceiver, instead of off the QueueReceiver. It also leaks out to malicious clients more information about your server than you might want them to know. In general, it’s better to just ignore bad messages and save them in an undeliverable queue for later inspection.

Alright, try it out and report bugs to me. Thanks.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-09-07.txt0000644000076500000240000000241711251323145023350 0ustar zedshawstaffTitle: Lamson 1.0pre5 Out I just pushed Lamson 1.0pre5 to PyPI for your enjoyment: "PyPI Download":http://pypi.python.org/pypi?:action=files&name=lamson&version=1.0pre5 You can also get it from "releases":/releases/ or by doing the usual easy_install. This is a small bugfix release that only adds one small feature. If you're running with the SMTPReceiver, you can raise a server.SMTPError with an error code you want and the SMTPReceiver will return that as the error code to the client. For example, if you do this in your handler:
@route(".+")
def START(message):
    raise server.SMTPError(550)
Then it will generate a reasonable error message (using lamson.bounce messages) and give that to the client. You can also give a second parameter to SMTPError that will be the error message if you don't like the automatic ones. Only warning with this is use it sparingly, as it then ties you always running Lamson as an SMTPReceiver, instead of off the QueueReceiver. It also leaks out to malicious clients more information about your server than you might want them to know. In general, it's better to just ignore bad messages and save them in an undeliverable queue for later inspection. Alright, try it out and report bugs to me. Thanks. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-09-26.html0000644000076500000240000001236111257445556023516 0ustar zedshawstaff LamsonProject: Quick Lamson 1.0pre6 Release, More This Weekend

Quick Lamson 1.0pre6 Release, More This Weekend

I put up a quick release of Lamson that fixes a bug in the gen project prototype. I recently switched all the domain names being used in the test suite to “localhost” or a similar nonexistant domain name. Why? Well, because some people liked to run the Lamson test suite or their fresh project test suite against a live SMTP server. The test suite would “send” mail to test@test.com and my own address, under the assumption that people would use the lamson log test server.

End result: Periodically I’d get bounce messages from test.com and other test suite mail.

Well, I made that change, but forgot to update a couple places in the prototype, so newbies were seeing the tests for their fresh project fail and wondering if Lamson was broken.

Sorry about that.

You can grab this 1.0pre6 release from the releases area, and you can also do the usual easy_install -U lamson to get it.

I’ll be doing some more work to fix a few bugs this weekend and working on a fun new site for guitarists, so stay tuned. And if there’s something in particular you want in Lamson just email the lamson@librelist.com mailing list.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-09-26.txt0000644000076500000240000000226411257445456023371 0ustar zedshawstaffTitle: Quick Lamson 1.0pre6 Release, More This Weekend I put up a quick release of Lamson that fixes a bug in the gen project prototype. I recently switched all the domain names being used in the test suite to "localhost" or a similar nonexistant domain name. Why? Well, because some people liked to run the Lamson test suite or their fresh project test suite against a *live* SMTP server. The test suite would "send" mail to test@test.com and my own address, under the assumption that people would use the @lamson log@ test server. End result: Periodically I'd get bounce messages from test.com and other test suite mail. Well, I made that change, but forgot to update a couple places in the prototype, so newbies were seeing the tests for their fresh project fail and wondering if Lamson was broken. Sorry about that. You can grab this 1.0pre6 release from the "releases":/releases/ area, and you can also do the usual @easy_install -U lamson@ to get it. I'll be doing some more work to fix a few bugs this weekend and working on a fun new site for guitarists, so stay tuned. And if there's something in particular you want in Lamson just email the lamson@librelist.com mailing list. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-12-12.html0000644000076500000240000002002511311017436023456 0ustar zedshawstaff LamsonProject: Lamson 1.0pre9 Is Out

Lamson 1.0pre9 Is Out

UPDATE: This used to be an announce for 1.0pre8, but python-daemon apparently has a bug that makes it not honor the TERM signal (still) so I had to go back to HUP.

I just got done knocking down all the bugs in the new Fossil based Lamson Support Site and cooked up 1.0pre9. This has a ton of fixes, but I’ll let the ChangeLog speak for itself:

=== 2009-12-12 ===
22:49:43 [c2be9dedc3] *CURRENT* Version bump to 1.0pre9 thanks to python-
         daemon. (user: zedshaw tags: trunk, 1.0pre9)
22:43:04 [1718d014a5] Stupid fucking python-daemon doesn't recognize TERM yet.
         (user: zedshaw tags: trunk)
20:24:20 [b5796e14a1] Pointed at the tag instead of just the leaves. (user:
         zedshaw tags: trunk)
20:00:38 [42768ade2b] 1.0pre8 release announced and done. (user: zedshaw tags:
         trunk, 1.0pre8)
19:43:53 [bb1b0a0dae] Updated MailBase.__nonzero__ to include parts as well.
         (user: zedshaw tags: trunk)
19:40:51 [2916b83307] Created ChangeLog from fossil timeline of commits.
         (user: zedshaw tags: trunk)
19:36:14 [41530d8a34] Version bump to 1.0pre8 for release. (user: zedshaw
         tags: trunk)
19:31:21 [dfe158b282] Initial support of using Delivered-To or similar more
         exact To header. (user: zedshaw tags: trunk)
19:14:13 [e9988679b7] Updated prototype.zip for upcoming release. (user:
         zedshaw tags: trunk)
19:13:51 [98e94b6103] Potentially fixed the unicode/ascii issues with shelve
         storage of confirmations and routing. (user: zedshaw tags: trunk)
18:56:35 [6224ae68e4] Updated docs about confirmation to fix a doc bug. (user:
         zedshaw tags: trunk)
18:56:14 [103454c43c] Make things clean up better on INT or TERM (not HUP).
         (user: zedshaw tags: trunk)
18:25:29 [009451cabc] Added meta tags to the site so it can be found easier. I
         didn't know search engines still use that crap. (user: zedshaw tags:
         trunk)
18:22:24 [8908d7852d] Applied starttls, username, password patch to server.
         (user: zedshaw tags: trunk)
18:16:01 [9a9f9f37b6] Applied patch for handling odd email addresses and white-
         listing what headers have email addresses. (user: zedshaw tags: trunk)
18:06:06 [7605ef0517] Patch for queue bugs applied [bug 9d330c8f80] (user:
         zedshaw tags: trunk)
18:02:50 [ef9808c4f7] Point at the right archive browser. (user: zedshaw tags:
         trunk)
18:01:39 [3b3dc9391a] Better logging message on failure to connect to relay.
         (user: zedshaw tags: trunk)
=== 2009-12-05 ===
18:05:40 [ddc4369b1d] Initial commit to the new fossil based support site.
         (user: zedshaw tags: trunk)
17:48:22 [8140fcad03] initial empty check-in (user: lamson tags: trunk)

This closed out a bunch of bugs, so if you entered a bug in then go make sure that it’s actually working for you.

Getting This Release

As usual, you should be able to install it with PIP or easy_install, and you can get the raw downloads from the releases section of the site.

Submitting Bugs

Keep in mind that this isn’t as heavily tested as other releases, so if you hit a bug, report it and I’ll fix it. Even better, if you supply a patch then I’ll consider giving you commit rights.

Potential Contributors

Most of the changes were from supplied patches, and if you supplied a patch and want to contribute to Lamson, then contact me and I’ll give you an account on the site so you can commit directly.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-12-12.txt0000644000076500000240000000701111311017422023324 0ustar zedshawstaffTitle: Lamson 1.0pre9 Is Out *UPDATE*: This used to be an announce for 1.0pre8, but python-daemon apparently has a bug that makes it not honor the TERM signal (still) so I had to go back to HUP. I just got done knocking down all the bugs in the new "Fossil":http://fossil-scm.org/ based "Lamson Support Site":http://support.lamsonproject.org/ and cooked up 1.0pre9. This has a ton of fixes, but I'll let the ChangeLog speak for itself:
=== 2009-12-12 ===
22:49:43 [c2be9dedc3] *CURRENT* Version bump to 1.0pre9 thanks to python-
         daemon. (user: zedshaw tags: trunk, 1.0pre9)
22:43:04 [1718d014a5] Stupid fucking python-daemon doesn't recognize TERM yet.
         (user: zedshaw tags: trunk)
20:24:20 [b5796e14a1] Pointed at the tag instead of just the leaves. (user:
         zedshaw tags: trunk)
20:00:38 [42768ade2b] 1.0pre8 release announced and done. (user: zedshaw tags:
         trunk, 1.0pre8)
19:43:53 [bb1b0a0dae] Updated MailBase.__nonzero__ to include parts as well.
         (user: zedshaw tags: trunk)
19:40:51 [2916b83307] Created ChangeLog from fossil timeline of commits.
         (user: zedshaw tags: trunk)
19:36:14 [41530d8a34] Version bump to 1.0pre8 for release. (user: zedshaw
         tags: trunk)
19:31:21 [dfe158b282] Initial support of using Delivered-To or similar more
         exact To header. (user: zedshaw tags: trunk)
19:14:13 [e9988679b7] Updated prototype.zip for upcoming release. (user:
         zedshaw tags: trunk)
19:13:51 [98e94b6103] Potentially fixed the unicode/ascii issues with shelve
         storage of confirmations and routing. (user: zedshaw tags: trunk)
18:56:35 [6224ae68e4] Updated docs about confirmation to fix a doc bug. (user:
         zedshaw tags: trunk)
18:56:14 [103454c43c] Make things clean up better on INT or TERM (not HUP).
         (user: zedshaw tags: trunk)
18:25:29 [009451cabc] Added meta tags to the site so it can be found easier. I
         didn't know search engines still use that crap. (user: zedshaw tags:
         trunk)
18:22:24 [8908d7852d] Applied starttls, username, password patch to server.
         (user: zedshaw tags: trunk)
18:16:01 [9a9f9f37b6] Applied patch for handling odd email addresses and white-
         listing what headers have email addresses. (user: zedshaw tags: trunk)
18:06:06 [7605ef0517] Patch for queue bugs applied [bug 9d330c8f80] (user:
         zedshaw tags: trunk)
18:02:50 [ef9808c4f7] Point at the right archive browser. (user: zedshaw tags:
         trunk)
18:01:39 [3b3dc9391a] Better logging message on failure to connect to relay.
         (user: zedshaw tags: trunk)
=== 2009-12-05 ===
18:05:40 [ddc4369b1d] Initial commit to the new fossil based support site.
         (user: zedshaw tags: trunk)
17:48:22 [8140fcad03] initial empty check-in (user: lamson tags: trunk)
This closed out a bunch of bugs, so if you entered a bug in then go make sure that it's actually working for you. h2. Getting This Release As usual, you should be able to install it with PIP or easy_install, and you can get the raw downloads from the "releases":/releases/ section of the site. h2. Submitting Bugs Keep in mind that this isn't as heavily tested as other releases, so if you hit a bug, "report it":http://support.lamsonproject.org/ and I'll fix it. Even better, if you supply a patch then I'll consider giving you commit rights. h2. Potential Contributors Most of the changes were from supplied patches, and if you supplied a patch and want to contribute to Lamson, then contact me and I'll give you an account on the site so you can commit directly. lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-12-13.html0000644000076500000240000001111111311261740023452 0ustar zedshawstaff LamsonProject: Small Lamson 1.0pre10 Update

Small Lamson 1.0pre10 Update

There was a very minor one line fix that corrects a fairly obnoxious bug in Lamson due to my not understanding how nonzero works.

You’ll want to update to the latest release if you are following Lamson.


lamson-1.0pre11/doc/lamsonproject.org/output/blog/2009-12-13.txt0000644000076500000240000000040511311261207023327 0ustar zedshawstaffTitle: Small Lamson 1.0pre10 Update There was a very minor one line fix that corrects a fairly obnoxious bug in Lamson due to my not understanding how __nonzero__ works. You'll want to update to the latest "release":/releases/ if you are following Lamson. lamson-1.0pre11/doc/lamsonproject.org/output/blog/html_email_in_gmail.png0000644000076500000240000012752011235004025026221 0ustar zedshawstaffPNG  IHDR3 Bb$iCCPICC ProfilexX 8?3c0jY6uZ2ƒRH("JZD=$Z*HH!d?C}}]߹ywy}~H~ti-@͍K"#zĘaؒ.q |㟌y?(s@҈k0e0z x30G,M!"8^7p#olyZ`22LCY`=?>KD.I!e #euαdAS#S`a :ج)/nbipmmFyԵܵ<PCh\F6=@!ax(a.:9՚m7W/7ͯ#&r8 5Rreq{N-+x+v+䩡]5jt= Ffi&fZ6V 2lWD'}~HqSogd^ F ARZ,=?rrQcrz NhĪ)>% (;{+9 ]MunԅGF.O}+jϵYrfrWs)w8vpABaΝw%rFe]+="=46z[X\-:gz[6ldkbiZ~~oh1iOWgzC_1|]@ {"C7F>?-~b; FoY-_"ts7oMSS}A01813?2;\|ӂB/_3KRԲJתjÚ5$CI" B2qa̯YOrDr*sl%HT_niA*R챔;/?(DU.WYPSU(֜Ж9WG676nogma@[BVi 14 ٽurlsjW@s. T7KdNG3gGЄo7/?% *?ȝlN7 c 2:2pґ([OxS{/S 夡ӭg%צ<< \ eKӪ.=Mox}ӕlDD5])po}ۢ]{ՊJKU4u+ XV:T?z\ɣڎuտy񢭱eg_W}.tw޿P=>Lh:"SWOCS0 X J㣵5!Aaa?K#B#U#79ewt/NJÎOɍ 38;9_*3!,>I4ҙg)gSS'^8u*"+2o\ͻ}9ZnV^7n?\[=-ERŚ%6e'*|~V( K-ilzsRCƋ&6LEDžnB޼/,`8O_F'&T-t-]^Ue#1rF K8w$$C([Um$.a $6@4A5'CHP UBm0s63pAvH0T6 &i6&4S}IfY9yň%k$k VmbóEaOf(Jf>ySfix޻ķǖD~6me.5ۃ%$aI"]b$K K/$;0B/93y9mRrJj 5O&%_04iTbĤô%ʊo-f#WdHu ?@sj6nd61 3«{7Z7Rȏ\u!+MRFעre BW$޹yoxwilJC5xPM[Z :GzM <=9]d% %p<}` BC8Ht: Kh`6)q*\(a1*uՁF5 ,a\:W=[/M 7Z'tw6x}~gvO Z M3w؈ʉ}L)u` nwcrG+)+)Q9qM3_RACE&f3˭Vц76}CL\q.xWq]:d=3d|E~#!1l);6%K9%f_lfpDtR)ŜhV.qj&jOF\oojʹZp>eko*دֵnKz={UsC#"_㨉酟 ˛ E "0$)A3 JJV3 B G'jjEs?31gĈb"1qK֭q?dK6c%{898 ]yp; Z]$b[xTa#Gm8kʽKX]*+YVO֨>~)ٹW |/\s>HW;MҺ?$V7 =?z0qϻ荚Y[0T&>1{ϏYEs 79,b˖VV VFDߨ`ޜ>QClmBVHt-!нHύCܞ>&1MQo rHo%lI3DrCrCKx ]?4NF,* 4B19 uMamc\ ~%jhqZЪCo2h ZVGT'Lq>Y |1wd9ֶ>F#=Ā7{Aƛ x9YYU/}2ڿ pHYs   IDATx \OiS)$*nFC~hl50 aFScma1&1Yfd B 7** ۽?޵ds>yys==wd_?򔡦wm*f:ebϫ;B4ΝÇ=ux-/^=zt}}z4x\gkh51LMd;, 4KzP {یMFH $I=7_*$@H]$IMM)5fˎVzmż )ܽo{+*k@nJmmTWɽ'ٯc.H $xԻC[SRGnNt]3CsE’' o{Cy<&=a7\Y6FܝjnU-Iӻڠg"$@{f,M-BۭoJOK3r6&T$_*Xu'Ov[]m+1'"m4 U۩1"'Ok]ɃQO{*V46 "䉐[T@fӤI[mEzai#Z_J,_.h׻~ ne>2勞`ĖkrT\SCg+3E9b5 Mͻv2V;\Cf:^[z̓|K2[5\Ŕf̋UD'G{#кbMFB&V}O&{)"z$VJyUkdEyCH#̼U}L{P_) l\:6hi+jQeBEί,z|Dv+wR΍U{,-)#݆EQ!gbvU{cfx!M6NYt`'YsC@32f 57%?Sޤ.Q7(}hGI{8YʧmYh{*`3]/|e^L![6I n0Q͂mV?aؘtvŀ&dY5ؘpPss 'ʊ 9f_Ù_F;>/I-P#iLi޾?59Wl(\1P&,O,r]&z%# ]W\w(] R¦Oo0lZS)AKUoF{cQ.0HUHkn; O(3N?X,]ikj)]<}R̊ku5X M@:X8k*NE&}2F݄~֝8@#?fHrzuhG37n카[6ҵHsRUT`oR![f%RAK5g;Ve;(Ҿ8K-R]zå nʚAͪ B,~8xk )h H?9xrTb"K(T_Jfh[Fj …;b:FF ה=xPʘBWBrSHGMBa"1@8@Bߊkׄ/|#e=]r Pz쾞;dbm Pi܉M{@o11 3T:[T .8'N697*<86Oߚ#ᑛ{N\~K޻\>t$Gמ 3 ?xUm^ԍ<{ mGJXr-M7H܃]B\V >4L/v4yʚIz[\JBeJ0}u%fcN;v=a4'Z-t259#u,B-?svqh H!WÎ*O|F.Xֶ&+N>}ܹs36w,{}jXTzu{ &~I՜y{>z坘5+͠ʏ,3cJwQ#SRydo3Z9g\/Jp++P3쾰I} 6{:M~v\2(HXI>oc[L҉VK4ݱÉۧ4fIu ̻/)\99o>,wDV:G]v+w?:o7;hl7TR=z arY>>s|}ImCٝJ'-YV'1ߧujkD_| F@uhI/~jr#:#É'_ c?Zw`& 6~tɧ#>S{gDHjAФjaRe?fɄ7oSMN$\fa&sW}س+K%,דnZe$~mdYnn8sRM c/KguI|,f|T1wi:a 5R,--]b (DNѱ EՊ>Ւz~]!L apn_.; YJ.)!Uֲ)BDw=5SOJ';xt3njoX^״Zu3 $i 0 ͯXfoV@*F|*P'YsY>H%RMEOJxyam2THAb4U#P[ j\ %y*Z-|JQJUfYcOjl&'4g+?nzܻѰ#E枾x] p!;nrN`K8%=U^OH0K $B#GIcFջw[n1&L`kkZFFMnChL]?՟zC9@"%ϼZzTNl#}qРIvadºgʦVW]`<;J?zЄi*.<kzRzXFN[l >RgwMq G)d@bk Qح׃:}ޓ^m2ݗ#H'3#m}-wNYVq0K& mpn߳9Ziҋ[D˶Lp^.+qDOHA$VQ򵠪Bڬ`4A|2d媕C\ 1Zv75-ד;Y]zӯW}ř"1lPOe3&nQ QGԒeW{sF`uz9JsZi/UfNҲuQ֐H(r;rj S7,}ԄJaiSOy|ԵNe足2 >zym=CnBXd9b-#1~{W׀u bʷsfP3E'5]`!EYbPP/YS&IXE>-M;g݌Q'~ 3JWw|y޿l`N%Yj#l_G+:C4nu!T* z1ԗ'hL Fm4V#XC?[7{qdY#l97"s\|tnP S`Ye.&O:fdMs{,0bݧ3LWK:0L[$k'6>f)5){ U_MUHڹ]6 #pi# aN͈NT8RuÁ$&,樓VCmt$Y \h)8x4ۯ>ܧNzYnEW`I3khтFw3g2vn3'B>}Hnz\M,u c͢>kA,O_;I's`aƼ}Hq_@w߱)Wv]BK\yLI-m J~>2uؾ^y2t jhLkϰ{;TO?n_/QTu(T<6qFV֭[YY׎L%Rྛ\>@"j5-?FGk앧c<.t+`vO1cP]X26S۵O?xEL:0UQG :WƑ?ߞal@N]/"H&Nz[+֋BsNmR[Ls`tRk]:TN Ӯi))ʻSp7qHUGEꟐB* 1EyOTR6Ea_q(dngΥ~V|? jUqG 5!~ܳIHEУ:[x˧$H{JfȯwkkSDƵRW#FpttǍ׭[7iyۖK;O<\Dy9u~q7K`t!p2`OJtҿgku'ppovLWKFfi3Ժ呖zkIɶ'H(}rG$`&MvMAìý'=[+}ӈPe'*/uZoIiY* tk,LDb(G wz+|-l4ǜg֦=3+Sۜ$VQR0qe-Oj z=78 ay{f S~(y 4<PQ?|LnGͨ#𙥃Ԛp(zUsxiԾĹd"1ⳍk afړ<_ dhm;j`ԩ#!Lu g`ay0UHe:zuϴS9 7u#]fdj ~eZ 96Veb}l(+ qm>hp'üe}w2ǭV;/ZgfvCG\&RE jj(M=ϏVW1T%u{0qHVsIi,mqk?q3?uq)FA@%k@ި~FɃDyB궞2n~a  PxuY% =|x4WO~o/_>o޼e˖5H]%8[~N{+t9+W@LAe|ޔq능0ء,zT_V CX$Y$?/lEms̝nL5|d$I2蚮CS~=MՋOۚFÐƝwN7x h?~ŪLD o<}}+7BX;zzN}2f#b!ou&ݲѣG5觾+џˌoɪ>Q+VB!b!I:yP2ZNZZ-7b@!deQXeFZ6!u,>azVXHnhsV aX:diz d:Qm3_ٯ5TmP" U=j#-QWovH*iH Pα 2:8Ūؗa~+W"JnTCIm TNeђD퍲$n d)v>NZh@-*i?dvI5TjJI**+nʂ JO/@1-VD/dBaa$@zfhSj*C@H Jw3_!X@H $y虽C $@o f1e;toh@qh[Byu<xGgt46 $x ̷D$@H!;L$@H- [Ih"@H $@hl&@H $@-$4 $xGgt46 $x h<{-0MDH $;@ށN&"$@o ޒB3@H wzf@'c@H zfoIGH $;@=wH $[B=L$@H ;D$@H-![Qh&@H $@dl"@H $@-(4 $xgt26 $xKgt@H 3{:@H %3{K: DH $Mxf>ϮVeL5,:"D>_0J1Y]l[AӀ)F ҘnNqY#].w?]"fKNK͂w gg w("e75|BH $j 4ᙉsͩS>$)Laζ[s\uEHHVu˘^d76"Kd"==⊠J9J;uMN'%d^C_~-5rP $),5J~hifN[x>z= )Hl uκ$r/Av|<)Kycu{lxzT/i\_Ѹ7l"⛉ts0@H *M0pHWg~#Iv7tݙ<s+=|&CCg={n9'&KFX3u6ϟRs&> ))9;Q4yy %+NHo::]&]-q8a42|Vh$'uy{%ϞQ˔j P%A._䋒434U.i[KLN8FbɁ@hDGhr(I;4gg̗SsߌYwqzSu(OgR .K+TfH $^*&qqv*sJ3Kn2GFVzu4fu1wYsMgs CtºYi~Xǧ~5$2D"RYr.&hq2OH=M]E^/!ޡDwj,ϓCfSwx 766I}" $`FC]Qm/uaN8e!\!_ȉLqUZK|r3~Hfæ[̞)3l$@@ #%D"#b2.Uޒ ]@pbC8C~]} yg+ A%FGl;6@:memD6e}Z_:ZA36NWb(J;06edR´GZ B)/̥MM$֔J;MP9D/mK*啫؟(NҥtW/&?ix(&y u]g^[O290'bYIO~"Rn-/7X~>kR>-)2H $^xf:X?#,}}-= ]8WO$#byDr_PԢ6tFaВTeMebu $1nΎ3֔\P1:!lOusB<9 f2ཟx5aGƱ(T!&)[>d7EռTI^@+ 6бC ښ޲f=V}0;}aPZ6}'qKn6՚=^PwiÝck8✈ ظf%@HhL>ㄯ<=r&?=ޠZ돛8果ۂe/)>=D+ ,g8|PYg{lR٭ ̝X)}uX@BZ̤˨K%|̴-WGݼ7H_ 87$5cgtZr'RDDq (dw4ЅO4iWΧBR›U(yTBِ0bd\~mMލм# K8/ҫk;Aa }'7[gO.쓥^psF`>@H &3#Wlۉ! Nbbޝ ]ږ8OqG99vdMbYȟ+UMXGe[b_9M$:vo}] Xp0Ś eتfX렫>Z*?mܥ 8|J@XVHL\jm=7Djl)Q$"Q,@mxYC|xLz7PԮ ǟ=h6'a;}s>dE"7D_>E!hlG,? lf"$xZ+ ޲fWe. ~1Qj*)"UYi*MoSV0_# d96)XHghh-3b>eg@Y[` 7O@7EGS/uR\Z#P.pdH?W8N~Fԇ<*Ci؄ g43_WU!J1;h^IAA`iF_h0AŘ@HЀO *,qG}el KPY#,c~5Cf BIcY"yu)oU3żJb+K[KTd?QΜe3Ѧ%ӝ9c\<n K%%d$X^vZy0n0J"$x^3{:iSt%oPyG&R$oaٿ+!H $g>Z߫l=? B}70!SxYYY$@H :虽:ϯY\W /!hjZZY͵g%@H 3{{mGH $]ZՌl @H $虽!f $@Hg $@o ޔ@;@H $$@H )3{Sz@H $zf8@H $@M  $@@H $BCΝ;1AH $h9ѣG7ngb|h]$@H 4MÇ7-V^x\Thkj OQ@H $T`\Uc930 $@՞YUUU fO>͠Ó'OJ$@H []}AT{fͶϞ=14+H $h9ovlL5,>zdjjjnf#Pʲ#N+]~h3%/sQ&+=1-AV[v5F5Ln'%O.nXMTV cThx=3.}U'oԷiUڪե} &r*RVP^|?=%) I)ijJ"#ZTJ\v0`Z|J8xY&XBVb" 昜/=`\]SibP-(UPSC $',{ b6$#KQy v/qs8$:{5c9*++km[{10;S_{WS񥾱? ?grY>;?={ƃŒ|d)ϩIQ9#7Ǯ ?Kiqq|]Mohx <97;!xwd[9HM._ނEg5;sV]2aIsfZQI3[V}qqEfmo̙_GDy8-;vW&&z;2`|ؾ;uIK5yz+xrH] n󥥰tC,?kDs\\2=c?\tmi]ސ $hţU CPr!.Шu(eۺ)T+tiU%w/[> 0$61/{bhbJ^w;¹I65W)>j*_eg ]0\`%tT4ʅl)DzT "$r5gt|³?=>bc:(o/O;TS)H~~v“֦˻N*>;c{CiݫN֤NGq ӥ `Dል7 ց?ɽ3zߨ!*5pI j-JeJ)$%$喱$AM8[b#K#\cde Qu) "{w&a_Jqs#;e0eWc@ R JcEUƸxq d6[o{3pY"zkDԔR !~^}-/sT\b8{_ₓ Ma!{}In&)hd;o"L?Vu! 2:r*HG|fiN<8c%. #m+$6&XODr=DDu/'.#z36q!EѲc_%s` 0떥t]Gr2-/b5$Z ,{`vDʽ"]L}z(sg缃.u09{ﰾڙx,>Of=N4q+SfǰZKbN+?v2Y2-#]AF(( :d@ N=31kլ4ʻF8ݨ"Y>+rwxX_3g&UWEbNPZvcx 'p2ېm3>]_ Mn;7QtK6Voq21:fep_}e˙IEϑۼo[wgӇۀ 1. r,ns>Զ 8plA :wi(,`۹~&"v6z-y\+c^P/'[YM$p6sbQoJ̐Cׅ?H <]}pl~:>}'40j=ڭSO{hu+Fk5+,\6,vM-&uꪧm6|=>אNRb8M\ͿF 1okҷya+V FO~x-7D{c]a ֕cIO^JIulIo, O/X}t0!kK﹤yysS,3IK)S˿H<;KΌ"+fTb dl1MlY`5+\y0&Vp;tչsGx ݳL;εkiN?BN) 23su6?*.'FvR׉HaIXLby dw^7vI;syLy[S~ cmVnNuI|lRuu`O=,udEE%|y!!Gg\M4X˽;ʣ:x wāl<[\0/#KA(cڅڲpM,%ibVW7R"sf6^p gHc6ϘuİcyosV?M!I9=)qq4߻A6MfY3rLv]]]Ph }ȌIׇ4j UB£wn?SSΈvtoo3zq~[tJ;0@Pnj2h䚸%R}CD`Om&U>3ooLTz!MruXzB tG˩#?jb.ݻv"U/VN³ 2ZUD8ؖ_At]g $QӒXy+sT1Icz n4 P[m'RUE<Ǭ^?lc&xNA SMX^Y6n~ԙmٴXutl&ma ړL_M]B$Q̊6WLAe/6%/D;b7Z*H` " L[7a_pt&5K9 \ 53#,&̐L,ģ!FW^_u|4c"8kK>VOuoԵ.ly\::SuSA`ne 6M>%eUpe\r]+uJb'پ/j9 "b01;w!Ƃ_<eyZ*34Zx&r3E:UDӴIMbٛKto*ȖGjÖ9w\ȳځޫe鵵ugKζI}vDy(QY~sk +k+ Zz ʒ= wEL )?3oeb\gڷOCʗWT폷0 $Ј@}[SqaZ']GC?\U!,bwG:wiO]joN7q.Yaŋ?ݸW-|#z\']ص_ X{{iZiڪ_kd:д&KcKxY@ Sk*Rʔᕦ#͌ͨ`l6l6I*_6Hdgr īF9H@ެcpOP$v,.N0#"MzL"&2x5n@yobr6PxAs_JT K%ܪR=P.kn.LoOp 8kTkԙ8}R}r|ؽ'#ǭ]:y{0]P%CXw;H .ʕ>6τ}QTA(r۵k' kxښ|a}1|L eƝJV%' z |ffo77ycb<$G7H`8(s/;Grx:~4ML} .HFgL+K)p[goXpۗ.=#ćBm}*Ys=IS: UɆ}\,qrqF(# z}3/yKOs|wVr|(.Fx+CPF6 m-MQ ތ:J_Ǫ?mo@Yٸboˌ"n9s݈ٸsK!9!(/WL.NP ovĂQwև  ^><빬$F[R&۸|.~tC1SI(j1&/YI% &g.u07g{Ro*u$KOi΄z:xxdž`G&x}4S>4u/w4`˜OUs DžB5-X}1ԙK1#;*$fug˸]TEg6hc7}sPw(]u;]yP9؜]gC{ O 2kXBM.[+ jeb?7]vmъYFVѶmeeP\Tdbj ~9s-XEԧT~sKSXGv5H>=3SI#z˓s]1CL$gJmav]=5?FHgpQꕶB6{fFPM{fŚC?lKAY =1|֎H#@}T ֫Z𹦖K6HW>?T ?& $OEtZkNiƪ}uX"tPg~mV*2_vdŰl"ѳr0&)[{l[VOGIXٗuԥ ʔ&院g5̲GiE[kj־j-/(}4u P+$Z[ܗ>RQQ, ߛli ~ɊKWDtt?:X V=8*v6 }W5H z}v7Z73$Hsylɯ7{Ĺ5mPwߥ!vp~jk)۬a~ M9^2~8+?++dh"E XFo>)ȫa׷hj !ogDߕ;)YUrNG; oUZٻBۉ@M73 YzxJ=:.=}6fegK:L4=?V@H 5AQv7^0 $@HhctT@H K Slhܫkzf@H $hهeû 9փ@H g!GH $"" $@H95G@H $gHc=H $hzf|$@H .虽.X@H $#Ys0 $@zf4փ@H g!GH $"" $@H95G@H $gHc=H $hzf|$@H .虽.X@H $#Ys0 $@zf4փ@H g!GH $"zYihh,U $@o;gϞ f hX $@zf+*EH $@+g hX $@zf+*EH $@+4,6LoI=_ [[Z;=/E qcʥR+E`y| 5$" 8qOHAZ*^agr*ΦU0+IgǨ\r-Nsch*gHZ\X嚱K5Vޯ[ V.WY/Hw)yO+G +kAuZW춥Ӭ"J``TNsʶXH,d< b!Mϕ?(M-@H 7?y=BPJ"}ܧ|s0Eu8 #mer:BSk$yqK/ %NJ8WLAʌ2i`56>Twܴon* P "E-2v_RvЪ_-R_Kll7>|u5ۍ KiZTP@HΙ3sfL2L| ss}8?/[dᝓuSۮ}kä[}xtӇ!.-N GlΣ*W/ ,ac&xåݭ/.Z>Ef?wr[2}yw\^vg򙱬;~_گ6ݕFS:[teՒ鶩aݙZڒ1&v^kzzNjw"6.nuk ͭbjμ^7^0'^1)ᘣX9y M^qe=#KF~v @ p34p7m3tM+c S5I˒_O[uHUM7&un%vLV2M*j GBU{ᷕ'09e02X}yRkikOs)ښͣK?1"J@8Q9pهjąvyݷtL)&`८Lj)b=ۖ:7ǦGkQ<]Ĺ}>%f*_xoɼskZ8fg Ɯ6t$|0ݰSO|޹yy E{uyI0\^أ^9M^CAP}+ZFuO7<aÆΡ xBU nKƝE]l [s߭unߴ7+SbqƖ%i,S>KQN쨴++ida_ivMoc@M :Z@r$Q )_YB[7o|9R]w>sѲpsLPxoN?JTYjhՖRoo 殴/jw*j3@(t NxyO/?om3{g^~yg3-<ۉpd_)mW{1԰K?S3odi6j{BԺy̔Pka}]豈%;]QBzA¦][Q[u8wY@@x/|>W6E=۲6dn<ۼܻ!)Tukrf__ZToJ6f漣#W7]KR33yM-{Ū=2\pYrAxz:wEkiv;WU{U9!׼| &ݽ.۴(27F?oطssb^ָ,޼x?XC@7_Xj>;B)g~ERk*DGw}]-- gVXgso?XeRh>`n ̔K;5C[K$ٴm,2o»-)3l,eRvZLG'ZxljLh@D46 2Kovl)%@&}"=*֟'^λs| o)޵Έ6t{؊6 po2,[\'/IXaݼEOog2A(-7o}ȸ ~:̄_؝/^ǫs 'g<:Iǥ.Қ?: ;zh^G+Z]~va] nq @ru~osʗ[™S^iO=̝=)͋.26ݭ/єg.,ZXI:IϮxm]i+^˵XfUmۿfK"~J^Sf.:qͼGǻ`[ڹ<B-Vu2GvM׵;|`_<& IDAT^LG'~hZMxAfof +@v)(Cc^wxS*|U'v'e@dR8pg̘+]IvO2z̬o5_xTl&Çz`|/hM.jw'VdnsGtgץ3.>M j,A׼f J[M<\qf?O=;a[lZȋiX99:ֺtyIttRO ^ 0JzS5*W~~(4 U&<1SF*D@̟gVtݡ^]TK7vg6>8kb_Y)+ @3nP]/ *L'Hx<M`֋!@"q>4?cC'g&unE96@h -n7Nᇇrp_L@¡ Y E?4nQ-*@8FEfwc 8 @@>3@@ _e$h  @d9 Y@@8@@| 2˗    /Df2@ 2@@E,_Fv  Df  @H@@Ȍs@"| ځ q  "@d/#A;@@"3@@ _e$h  @d9 Y@@8@@| 2˗    /Df2@ 2@@E,_Fv  Df  @H@@Ȍs@"| ځ q  "P톄i eԐq$_4ٷՏ¾RIw$  @N?''xR]2/!ſ^Ԫӎgkk֕&*706@@ CSW#)iǓQGFēz  s弇T YD@ 2+1  P(Df2R@_Ȭǘ" @d9q:GoKծs[EނUz|Uխ1{5Ag)]O@8̎U?km-Ng+Wks ]ejKQ.Q8~T)H@@2$ý6Ρ҄, >uRd'mAaWo5 E-9.Rgy`_GUZvwyYs6~bٷOVN:?x|  PH6(瘗ג~wSVMثS&J-\[G&itZR/n:ME*  F W3TQrW^I }|JGkźv"yu%ks mf])\@5YHB@;2ۙ24y&Ήk-Mf HL9-3a^aӠ:z}V4izOR  @E4rrYtsqlwi/4twk7$xy3 xKvpGf9Z;'KSҥT<9C)bl" &9´'tɝik1w=yG=#'%lj@% N>nk)F@0}Pai $ ^Xκ%NA%7=vYz b/jp٬RD@N"kP6rgfu|=U%Ҷ,- ;@@ #_̨Alq~9f⮫#'Դ[W3՞&y^!垲!d&@L;qV fZ/8z4ضtZoO7Ji) IIl" 6%UɝS[jo/G<Ǽ}֫ɇT֛s'꘷?WȖhj78"ASkəF@Ic~oitmu*;1U-A!ԟu 5yW#bI{魵ZQ?;%v(%@@7|̴{>w~;.Td-mИBRS0Y2,_n$# i{4Yܙ;ue&gpNW?rMcg>R.eJN@8ёJ},_tܩ/ݩ5{0x&>y:f I  Ч@#WӺSؙ^M/Чg)1\A1L XNZk'tYk,-qi]gh$e0l! @zzzR38p`3fHݕ.eەoLud[4b>13i둰ݨsmphKٿmȤA@'x@9W:uQ2~RyB*ܠ?&  @~53K@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  Df:2 @sz *@d#C@@`  1  %jXϿg$A@ _csfǢ1  @.rJ     2˅*e" "@dv,j B 'YR\WeimK,{*ݠPb$mR#-L6C gXU"iF )K-;Hu&e菧Y/WSJ?4}(mpW9r]{X;?k6)ܣ'Th'>=Gc| ->lОiR]RKf,=+5SĢjBڧmRGSѡ*TX{Lzj2C斴rLڜ´,.vJ=CIidtӛsfܦ53QAUT}[ᚻ̶i)>n+Te>v6uy 9jODKB:d )t9@(M{Le/93hd/3ܪ>rwzyqL0/1Α^0jWY&sNNN%u>?<M-N ifbpW٩\]:X I9ciEpnѩĤ>>Mu3gF@fS;kW?ZpԁICTiXQ^`t'SXI'ߓF-=KHsj}iLLC4kqJبԹH?8wsqg]2?S!^|"Fm&?&?;줓1pyYm?;\5;9͛ZQEzuYl'NB!f:y6%Rm$Sʼ!5Ü}^vockᕹQXN 4mwʉxUHMGnm- DdmҲږ;-oU6nSnT.w۝pۜ9f ۝A[4[7AFs>{wŔZg$7$uڬG3fcxyLK'FΌ4d^mZg[֙@e;͎NQpf+Qn>&ٞz*UnyQU{Դ_J ?"x#6?hNXof:y,cX5:,O<&JE'zr~Ώ=&%& P1V؜3d=2 *&:1gvHoİ[3WZirBJL^zFf!&0{Vsj\|6TEuer.MFC\iY.9"Ͷa]U!e"fE}3wYry_Nc/ug}f"kb2nPK&w7xu4W-"frKڶN`\l7u=j97sw Gv6 Ar/hm]gnSY'Ƶ}Re͉ۜJ6){}"*uy'm>=1;!f,HXصթ 9C9iQ6MdSi? >ڈs"c eTEn3T֨Q McΑ iӰ=aCXUuVJԽsW8-8U79Nh*'Ed?|$"@1 XicYfNέ,Zzg*}V rر5Tu3Qj 7BoSm2X,K.5׽.W&}A{i85Aӌf:L5e{FUfcRӡKLd- 령I-Ǥz]+Qis熫qem1zl TWh$=cp(Z6Nݍ2A[SYF'зd7)Y'W/6w1מmOkm-osi{bܬ'F{!stYZl{jS{Mq.e8 I۽6Gf wwgݪ wyʜRzq_`~햄bݍS+}E65ܦ!13dhf%v#wdDۘ!哻yQ"ziV/vMUd~hb2I8˽Dp595iR汼+6hbQr>֬]W7N EoIz {O3 ^+;&H{.ү͆?Fh/]v|{~Ƭ-GM1kL#ԋ^rzf?R3ߔluby|:Dl^%gq;8ԏo3Co =M1'6SW~xtv8CF)0VLodM[L3?!ɱκWO_WDtIkCpN^-ߨ sy.`ȼnEs1ELXҌ>%1DŽwL9UcEt"i ՄO'8G?Vyʶ SebSb'YxY?b ?o&`A.r[&sD* @Tt~ք;59+!M a0.u״7XiW'"f{ۼ&{||f+Nk}dtؾYzkN};(vSsZ>W~~=nӥM]F' }.7z2W!et^ ,711 G-G}('VڤoW{\80nUnG4C5jK&c0z 8'I/CkWXꆥCW(DaS-Fӝt*=c%ʰ&hA4_7FgK-||3-ο#эusLsęY?=jfAH++Y/ 7;M~e>/4s4շ8x[z4O7Z35%S+M=iڥpce'F6T>kӪڣu6RWjR{et ֻS%/|VSi.;Mo@ю }{gyPcޝ{'1e&*17۹.{hHMν͊lqvzG5TFBАşvr7E6CNy\(߶fۤ'pSURğ4?8lʏ=دɐZD$=o^;OڴžуʴCٜ?C6Sʏ+gYAb$ƞɳCòo.E {_釶Ce-^L4L 4\}}F4L+l˺&%7)3b>._~e-˙]Xkc;K2mFW!K_nM({pmӐ{t#gtĩz{g5ow|" 'Z ˑ.vuJ/iZ6F7OSoYE΅ O.*MӀ&5ifК;מCЪ#F@N@6#gޘ;zbO_M`ze<լҽ @@P 7>GsǼr0¬ΟTl[ M&  ًJ5&M# @>ddϫi~!ғl@u|QվoKFk#ƍ>Sm|H!@N@Zo2= aI}V^{eYPoMBZcEX%LIԄKL>@@-l%޳ͿOS~㽫b9ԇ^eN/wR]==լ̺ ՜R1ɱw_^uv/4XE@+0'X~;xy9]{u}_K5ιНGsQ">TᆬZUלB@@`@ <bXzԩSGՔ5M-WKu}ٶ 2K@@u53]K{Wl絀/O@(\>[uVMΩ% w9:мKBh(mD@Ip̎{ Y ME@" 2+{  P@Df4X4@\Ȭ! @ `T@(r""` $@dV@ES@@̊| Y ME@" 2+{  P@Df4X4@\Ȭ! @ `T@(r""` $@dV@ES@@JտVI ̎Ec@@\B2@@c 2;5A@r!@d UD@EX8@ȅ@."UZBkҚ m־pR7)-NJM޼n4VL~[lU_&laR^78|ӟĺzޗ bGe* @OI&$gʼHzBjrQj6q^jWmѼrf=*[T/"KMxEJU:]6:Ek-mst-6GBjܭ6L'+ƒJ/umkrV_Y*ǐnV.1bVʵ6fU%N}յĴ!6G!U(Rclw3nk֜"u!#ޥ:^_sX3%|@oASwˤ+uIQd|x|oz݄nR|y)ֆ#]M _ñȬlmr#%D72PM*zw4-cʻT܆-jknB3p&ܧ*&쩨[٧5ΦkTCs묉m׋U\'szjY"MNV6 [~NͭwbӰwbMl6I"3aהۼu0N֘јfmG:0d^b-a@$"\\L;x\&ԣxn4fO߶h Xw%/EIiBwjѭ ~GW4Bz/5|_j#Eu>L-eɍzT<`"՛'=նhM_jEr}umZP?m/ӈ`O=żR-6xĻ i.攪nY#-T#{TgciYg<[MfGIpdƮ̺\m,3&ph4iBux_֘Vmw@R+O@@~of&\JenjJXkP)yB.mhm/w15vq?h Ф(=@Ŵr;dt١^/҃YOkb2FZv[߭Zw\v/JꅷP&Խ0vp|c-->ꄾNEq7)}h]Hw^jjf{K%`V@(Z~3saDeU]Ѡר- \8,4_e]fA7lN |K{ st/\y4Gej-[tsvՐnr  @Gf^w,oڼӭ2oLF`gl@hmw]xTa@$ƞd2X~3RwM4(a5Q[NK t[&Ruۗ.MtyY\gÛG]Lzk-3MǤ"͠>3&yX+V%qsh,  @PeW:uQ-2K`PFfuy.5G@82tCy/Y' P@Df4XԆɾ˸8ڃ @! ϳG@@l eS@@ 2;=E@)ݕfQ  P%ˠ?̙eD@@_J@@ 2@"   /DfL%  @Df @"~a@@ " Ȃ Y0S   YHdA@EȬ_@@,$  "@d/T d hF@@`<~,dX8@ȅY.T)@8"cQ@@ DfPL@@X̎Ec@@\dLY}d_҇z:EvW:mn) M"  P9s BR^+u9Q  (4G?SqX+??ԮVt]C&-@@`$2u:M6Rٹn-OGz25zɑNug@P9*jV7iZH-֦͚ަiZ.S0>Y@@+,\$|}LS.*p_2 vWIXOިJu˿j :  úA5sJ=pʋt ל̐gVI4Bs@@@NyJ˴AyaZzJόK7J+uCzD@@.̖|G NcӪ ]q~kǴE?sv\*5Aj2F# Ed6%SwmnmSQejnяM@  A====pg̘+mʠAiw%03:Rc @(+]W^=uQFb,]M1LRSIA@@NH@ȚY()@8N"p@@ kDfY @@8̎@@ dAweM @k2sf @"~a@@ " Ȃ Y0S   YHdA@EȬ_@@,$  "@d/T d @dY@@~ 2f*A@2 2,  @ 3  eD@@_J@@ J2ȓQ @@ sfi`HF@] ksfGIC\tw2O.wdj]iR @##[/?4co޷:CpoSz[W[e~{nn{Ug1X@@Bie{_{5Zg/uIv$_X;I~PI  vdvec./m|[>{+ӿK>[o`C|kw}##ηtI86w2 P,Fugl({׽r'2/gi~iՊ_72of^1Dϫ6MSڪ?|X8o}eXYA@|Yڞ>;׌k 7VFòx/Dò)S&}M?{zӻfdx(tuxOM 5- ˜=Q;~h,  @^ d52 Ͷw ?egCv?-ә7߻ګuvg诿/~kN- w|koL7;lv|?6Ƈ{i?밟,  @> dj[_u/46l|͒N&>=oq&^pHgɨ3LtC{zKBLyMd"ݱYx>/ijh^)m1L 4;y'}`g/cko.%c=ye_S㯽TO+ho SҰw]ޚ8~zOM\5fo,8+1PV@@ dism5Kca,9} e?G_EGwMs4aY8d̫8>o]8߽<{KPw>-zrP65 o[xSkNCxWypw,pwpO9 .T@@ 7ƕR@@ 2;z3@@r#@dWJE@^ kmnGN@@_9cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! YD@ 2+1  P(Df2R@_Ȭǘ" @HN@(~"cz "@dV(#E;@@̊! ,~f ڍIENDB`lamson-1.0pre11/doc/lamsonproject.org/output/blog/index.html0000644000076500000240000004533011313464306023545 0ustar zedshawstaff LamsonProject: Lamson Project Blog

Lamson Project Blog

2009-12-13 : Small Lamson 1.0pre10 Update

There was a very minor one line fix that corrects a fairly obnoxious bug in Lamson due to my not understanding how nonzero works.

Read more...

2009-12-12 : Lamson 1.0pre9 Is Out

UPDATE: This used to be an announce for 1.0pre8, but python-daemon apparently has a bug that makes it not honor the TERM signal (still) so I had to go back to HUP.

Read more...

2009-09-26 : Quick Lamson 1.0pre6 Release, More This Weekend

I put up a quick release of Lamson that fixes a bug in the gen project prototype. I recently switched all the domain names being used in the test suite to “localhost” or a similar nonexistant domain name. Why? Well, because some people liked to run the Lamson test suite or their fresh project test suite against a live SMTP server. The test suite would “send” mail to test@test.com and my own address, under the assumption that people would use the lamson log test server.

Read more...

2009-09-07 : Lamson 1.0pre5 Out

I just pushed Lamson 1.0pre5 to PyPI for your enjoyment:

Read more...

2009-08-22 : Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage

I happy to announce probably one of the last few releases before I officially put the 1.0 stamp on Lamson. This last 1% of the things I want to do takes a while, but it really puts a good shine on the project.

Read more...

2009-08-03 : Lamson 1.0pre2, HTML Email, Standalone

Lamson 1.0pre2 features two features that might signal the end of the beginning or the beginning of the end, depending on your perspective: HTML Email and Lamson Standalone. HTML Email support comes from a new module lamson.html that gives a nice template method to send out HTML to victims…uh…customers. Lamson Standalone will be a way to run Lamson as your customized email server instead of another server like Postfix.

Read more...

2009-07-20 : Lamson 1.0pre1 Released

Tonight I’m releasing Lamson 1.0pre1 with all the latest improvements I’ve made while making librelist.com and taking feedback from a few people using Lamson. The goal from now on will be to basically squash bugs and write docs, with only a rare feature or two as I find them needed on projects.

Read more...

2009-07-19 : I Blame Bounces For The Delay

I was going to release Lamson 0.9.6 much earlier than this, but as I worked on the bounce detection feature, I realized that if I just wrote that and a few other cleanups and features, I’d actually have a 1.0pre1 candidate on my hands instead of a 0.9.6 release.

Read more...

2009-07-14 : Lamson 0.9.6 Sometime Today

I think I’m at a point where bounce detection and handling works really well and is ready for release. I’ve got it running on the latest Lamson demo librelist.com and it works. I’ll be pushing out a new release of Libre List in a little while that does some advanced bounce handling, and if that all works, then I’ll push out 0.9.6.

Read more...

2009-07-09 : Lamson's Bounce Detection Algorithm

I just finished committing 0.9.6 code for doing bounce detection and analysis with Lamson. It’s part of the new mailing list example I’m coding up for the 0.9.6 release which I’ll be running on a free mailing list site I’m going to release soon. In this blog post I’d like to go through the bounce detection algorithm and get some feedback and samples from people. So far it works great for the samples I have, but I want it to be fairly bullet proof.

Read more...

2009-07-07 : Lamson Mailing Lists Down

I’m making up the release of Lamson and doing some server maintenance so I took down the Lamson server running the mailing lists. I’ll let everyone know when they’re back up.

Read more...

2009-07-03 : Article In The Reg About Lamson (By Ted Dziuba)

Just a quick update to point people at an article in The Register by Ted Dziuba entitled Lamson – email app coding without the palm sweat: Doing what Java never did. He interviewed me about Lamson, things I think you can use it for, and other fun stuffs.

Read more...

2009-06-26 : Lamson 0.9.5, The Push To 1.0

I just released Lamson 0.9.5 with all the major Unicode refactoring done and working. This is an important release because 0.9.5 is where I declare that I’m pushing to a 1.0 release and the base Lamson APIs won’t change under penalty of death. In fact they can’t change because I’m using them myself in a few applications.

Read more...

2009-06-22 : 0.9.5 Almost There, But Stumped On Templates

Since the 0.9.4 release I’ve rewritten the main part of the decoding parser so that it’s much cleaner and handles more edge conditions. If there’s one word that defines what makes MIME horrible it would be “edge”.

Read more...

2009-06-20 : Lamson 0.9.4 With Unicode Super Powers

Lamson 0.9.4 is out and it’s sporting a completely rewritten and meticulously crafted encoding system. With the new lamson.encoding code Lamson can now decode nearly any nasty horrible encoded spam or mail you hand it, turn it into pristine nice Python unicode strings, and then output sweet clean ascii or utf-8 in a consistent way.

Read more...

2009-06-14 : The Mailocalypse Is Upon Us!

I’m currently polishing off the two final features before I start going for the Lamson 1.0 release. I’ve been using Lamson to make a few little cute applications and create one thing for a potential client, and so far I haven’t had to change much since 0.9.3. It’s great so far and I hope that Lamson 1.0 will be a fun release.

Read more...

2009-06-09 : Lamson At NYLUG Python Workshop Today @ 6:00PM

Just a quick reminder that I’ll be presenting Lamson to the NYLUG Python Workshop today at 6:00PM. The event is at the NY Public Library Hudson Park Branch, 66 Leroy St., NY NY 10014 in NYC and you can find out more here.

Read more...

2009-06-08 : A Screencast And Docs On Deploying Lamson And OneShotBlog

I just finished writing some new documentation on Deploying Lamson and OneShotBlog that shows you how to install Lamson and all required software into a completely clean Python 2.6 and virtualenv configuration. The instructions take you from nothing to a running oneshotblog.com installation that you can play with and hack on. The process of doing Lamson deployments is still a little too rough for me, but these instructions should get people started.

Read more...

2009-06-06 : Lamson 0.9.3 Is Out And Sexy As Hell

This release is the result of me working on my little oneshotblog.com project while tweaking and refining Lamson as I go. The end result is 0.9.3 didn’t have a lot of big code changes, but all the tiny little changes add up to a very nice release. The highlights of this release are more secure server runs, better character encoding handling for headers, various cleanups in how mail is queued, and fixes for Python 2.6 support.

Read more...

2009-06-04 : OneShotBlog Sample (Hack) Running

I finally got off my ass and put the OneShotBlog sample up. This is the code (plus a few little tweaks) from the sample that is in the Lamson source running on another server. It’s using all the features of Lamson, include the new Queue Receiver functionality. So far it’s working great considering I’ve just been hacking on it on the side to try out the usability of the Lamson APIs and not really taken it seriously.

Read more...

2009-06-03 : Lamson 0.9.1 Out, New Docs

I released Lamson 0.9.1 today so please grab it and test it and shoot me feedback.

Read more...

2009-06-03-2 : Lamson 0.9.2, Test Coverage 97%

The 0.9.2 release is out and ready for everyone to easy_install. I spent the day getting rid of my tech debt by boosting the Lamson test coverage to a whopping 97%.

Read more...

2009-06-01 : Lamson 0.9 Is Out, Find My Bugs!

I just pushed Lamson 0.9 up to PyPI for everyone to grab and break. This release features a complete redesign of the routing, state handling, templating, and a full set of very complete documentation. Everyone who was using 0.8.x series should be able to migrate to this version with some work, but it won’t be terribly painful (assuming you have unit tests).

Read more...

2009-05-31 : Lamson 0.9 Later Today

I have been working hard on the documentation and scrubbing the code for Lamson and the 0.9 release coming out soon. The only things I feel I need to do before an official 0.9 release are:

Read more...

2009-05-28 : 0.9-pre2 Up For Testing, Docs Too

First off, my apologies to everyone if your RSS reader went crazy today. I include documentation changes in the RSS feed so that people can easily track updates to the Lamson docs. However, that means when I’m writing a lot of documentation it hits the feed repeatedly.

Read more...

2009-05-24 : Features For The 0.9 Release (Soon)

I've been hard at work cooking up the very nice new routing system, and I must say it is rather tasty. I've gone and created a whole new routing and state management design that uses decorators right in your handler modules to indicate how each state will expect mail addresses.

Read more...

2009-05-20 : Lamson Project Ideas

I wrote a blog post about project ideas for Lamson on my personal blog. Head on over if you’re looking for something to hack on, or just want something to read that isn’t about the web.

Read more...

2009-05-19 : New Site Look, Same Great Content

This is just a quick update to say thanks to Ken Keiter for creating a new lamsonproject.org site layout and design. The new site should be easier to read, have more breathing room, and look easier on the eyes. It’s even got a logo:

Read more...

2009-05-18 : Bug Fix 0.8.4, Mailing Lists, Spam Blocking

A few announcements from my work on Lamson the last few days. I managed to fix a bug, put Lamson to work doing Lamson’s mailing lists, and use Lamson to do some spam blocking on my own email account.

Read more...

2009-05-16 : Lamson Project Site Launched

Today I launched the Lamson Project site at lamsonproject.org and started filling in the content. Lamson is really turning into a fun and useful project, and hopefully the site will get other people interested in it and using it.

Read more...


lamson-1.0pre11/doc/lamsonproject.org/output/blog/index.txt0000644000076500000240000000015711203535414023413 0ustar zedshawstaffFrom: Zed A. Shaw Title: Lamson Project Blog Content-Type: text/blog Item-Template: input/blog/template.html lamson-1.0pre11/doc/lamsonproject.org/output/blog/template.html0000644000076500000240000000014511203575635024252 0ustar zedshawstaff

$date : $title

$content

Read more...

lamson-1.0pre11/doc/lamsonproject.org/output/contact.html0000644000076500000240000001047711230770143023147 0ustar zedshawstaff LamsonProject: Getting Help With Lamson

Getting Help With Lamson

There are mailing lists available (running Lamson of course) that you can join to get help. Keep in mind that these lists are running Lamson, and since Lamson is still a work in progress they might be faulty.

The best way to get help with Lamson is to email me directly. You can send mail to zedshaw@zedshaw.com to ask for help.


lamson-1.0pre11/doc/lamsonproject.org/output/contact.txt0000644000076500000240000000064011204533220023003 0ustar zedshawstaffFrom: Zed Title: Getting Help With Lamson There are "mailing lists":/lists/ available (running Lamson of course) that you can join to get help. Keep in mind that these lists are running Lamson, and since Lamson is still a work in progress they might be faulty. The best way to get help with Lamson is to email me directly. You can send mail to "zedshaw@zedshaw.com":mailto:zedshaw@zedshaw.com to ask for help. lamson-1.0pre11/doc/lamsonproject.org/output/css/0000755000076500000240000000000011313464573021416 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/css/code.css0000644000076500000240000000635511213001040023022 0ustar zedshawstaff.hll { background-color: #ffffcc } .c { color: #0099FF; font-style: italic } /* Comment */ .err { color: #AA0000; background-color: #FFAAAA } /* Error */ .k { color: #006699; font-weight: bold } /* Keyword */ .o { color: #555555 } /* Operator */ .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ .cp { color: #009999 } /* Comment.Preproc */ .c1 { color: #0099FF; font-style: italic } /* Comment.Single */ .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */ .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ .ge { font-style: italic } /* Generic.Emph */ .gr { color: #FF0000 } /* Generic.Error */ .gh { color: #003300; font-weight: bold } /* Generic.Heading */ .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ .go { color: #AAAAAA } /* Generic.Output */ .gp { color: #000099; font-weight: bold } /* Generic.Prompt */ .gs { font-weight: bold } /* Generic.Strong */ .gu { color: #003300; font-weight: bold } /* Generic.Subheading */ .gt { color: #99CC66 } /* Generic.Traceback */ .kc { color: #006699; font-weight: bold } /* Keyword.Constant */ .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */ .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */ .kp { color: #006699 } /* Keyword.Pseudo */ .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */ .kt { color: #007788; font-weight: bold } /* Keyword.Type */ .m { color: #FF6600 } /* Literal.Number */ .s { color: #CC3300 } /* Literal.String */ .na { color: #330099 } /* Name.Attribute */ .nb { color: #336666 } /* Name.Builtin */ .nc { color: #00AA88; font-weight: bold } /* Name.Class */ .no { color: #336600 } /* Name.Constant */ .nd { color: #9999FF } /* Name.Decorator */ .ni { color: #999999; font-weight: bold } /* Name.Entity */ .ne { color: #CC0000; font-weight: bold } /* Name.Exception */ .nf { color: #CC00FF } /* Name.Function */ .nl { color: #9999FF } /* Name.Label */ .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */ .nt { color: #330099; font-weight: bold } /* Name.Tag */ .nv { color: #003333 } /* Name.Variable */ .ow { color: #000000; font-weight: bold } /* Operator.Word */ .w { color: #bbbbbb } /* Text.Whitespace */ .mf { color: #FF6600 } /* Literal.Number.Float */ .mh { color: #FF6600 } /* Literal.Number.Hex */ .mi { color: #FF6600 } /* Literal.Number.Integer */ .mo { color: #FF6600 } /* Literal.Number.Oct */ .sb { color: #CC3300 } /* Literal.String.Backtick */ .sc { color: #CC3300 } /* Literal.String.Char */ .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ .s2 { color: #CC3300 } /* Literal.String.Double */ .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */ .sh { color: #CC3300 } /* Literal.String.Heredoc */ .si { color: #AA0000 } /* Literal.String.Interpol */ .sx { color: #CC3300 } /* Literal.String.Other */ .sr { color: #33AAAA } /* Literal.String.Regex */ .s1 { color: #CC3300 } /* Literal.String.Single */ .ss { color: #FFCC33 } /* Literal.String.Symbol */ .bp { color: #336666 } /* Name.Builtin.Pseudo */ .vc { color: #003333 } /* Name.Variable.Class */ .vg { color: #003333 } /* Name.Variable.Global */ .vi { color: #003333 } /* Name.Variable.Instance */ .il { color: #FF6600 } /* Literal.Number.Integer.Long */ lamson-1.0pre11/doc/lamsonproject.org/output/css/style.css0000644000076500000240000001524711203614744023274 0ustar zedshawstaff* { font-family: arial, verdana, helvetica, sans-serif; font-size: 100%; margin: 0; padding: 0; } .clear:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } .clear { display: inline-table; } /* Hides from IE-mac \*/ * html .clear { height: 1%; } .clear { display: block; } /* End hide from IE-mac */ html, body { background: #83b3d8; color: #000; } body { font-size: 75%; text-align: center; } #serious_black { padding: 10px 0; margin: 0 auto; text-align: left; width: 880px; } a { text-decoration: underline; } a, :focus { outline: 0; } a { color: #ff7f00; } a:hover, a:focus { text-decoration: none; } h1, h1 a { color: #000; text-decoration: none; } h1 { font-size: 200%; font-weight: normal; } h2 { font-size: 100%; font-weight: normal; } ul { list-style: none; } form p { padding: .4em 0 !important; } a, a *, button, button * { cursor: pointer; } textarea { overflow: auto; } button { background: #ff7f00 url('../images/gradient_bg.gif') left top repeat-x; border: 0; color: #fff; font-weight: bold; padding: .25em 1em .3em 1em; text-align: center; } code, pre { background: #262626; border-left: 5px solid #363636; color: #fff; display: block; font-family: monospace; font-size: 120%; line-height: 130%; padding: 15px 15px 0 15px; width: 505px; zoom: 1; height: 1%; } blockquote { background: transparent url('../images/quotebg.gif') left 10px no-repeat; color: #888; display: block; font-style: italic; line-height: 180%; padding: 20px; text-indent: 3em; } .ri { text-align: right; } #logo, #sidebar { display: block; float: left; } #content { width: 540px; padding-right: 50px; } #nav, #content { float: right; } #sidebar { margin-left: -2px; width: 253px; } #logo { padding: .7em 0 1.6em 18px; width: 280px; } #nav { padding: 1.5em 0 0; text-align: right; text-transform: uppercase; } #nav a, #nav span { display: block; } #nav a, #nav .act { float: left; margin-left: 2px; } #nav a span span span span, #nav .act span span span span { padding: 6px 10px; } #nav a:hover, #nav a:focus, #nav .act { background: #ff7f00 url('../images/gradient_bg.gif') left top repeat-x; } #nav a:hover span, #nav a:focus span, #nav .act span { background: transparent url('../images/metanavtl.gif') left top no-repeat; } #nav a:hover span span, #nav a:focus span span, #nav .act span span { background: transparent url('../images/metanavtr.gif') right top no-repeat; } #nav a:hover span span span, #nav a:focus span span span, #nav .act span span span { background: transparent url('../images/metanavbl.gif') left bottom no-repeat; } #nav a:hover span span span span, #nav a:focus span span span span, #nav .act span span span span { background: transparent url('../images/metanavbr.gif') right bottom no-repeat; padding: 6px 10px; } #nav a, #nav a * { color: #000 !important; text-decoration: none; } #nav a:hover, #nav a:hover *, #nav a:focus, #nav a:focus *, #nav .act, #nav .act * { color: #fff !important; } #middle { background: #000 url('../images/captl.gif') left top no-repeat; color: #a4a4a4; } #middle .middle-tr { background: transparent url('../images/captr.gif') right top no-repeat; } #middle .middle-bl { background: transparent url('../images/capbl.gif') left bottom no-repeat; } #middle .middle-br { background: transparent url('../images/capbr.gif') right bottom no-repeat; padding: 30px 0 50px; width: 880px; } #middle h1 { color: #fff; font-size: 240%; padding-bottom: 1.2em; } #middle h2 { color: #fff; font-size: 200%; padding-bottom: .3em; padding-top: .5em; } #middle h3, #middle h4 { color: #fff; font-size: 110%; padding: 2em 0 .5em; } #middle h4 { font-size: 100%; } #middle p { font-size: 110%; line-height: 160%; padding-bottom: 2em; } #middle .teaser { color: #fff; font-size: 130%; line-height: 170%; padding-bottom: 1em; } #content h3 { background: transparent url('../images/dashed.gif') left top repeat-x; margin-top: 2em; padding-top: 2em; } #content ul li { background: transparent url('../images/li_dot1.gif') left .5em no-repeat; display: block; padding: .4em 0 .4em 18px; } #content ul li ul { margin: .5em 0 0; } #content ul li li { background: transparent url('../images/li_dot2.gif') left .5em no-repeat; font-size: 95%; padding: .4em 0 .4em 15px; } #middle table { border: 0; border-collapse: collapse; border-spacing: 0; width: 100%; } #middle table th, #middle table td { padding: .7em .5em; } #middle table thead th { background: #ff7f00 url('../images/gradient_bg.gif') left top repeat-x; color: #fff; } #middle table tbody .odd td { background: #262626; } #middle .in { background: #fff; border: 0; } #content label { font-size: 90%; font-weight: bold; } #content input, #content textarea { padding: 5px; width: 530px; } #middle img { border: 5px solid #fff; display: block; } #middle .img-left { float: left; margin: .3em 20px 10px 0; } #middle .img-right { float: right; margin: .3em 0 10px 20px; } #sidebar h3 { padding: 0; } #sidebar .box, #sidebar ul li { background: transparent url('../images/dotted.gif') left bottom repeat-x; display: block; padding: 0; } #sidebar .box { background: #c0d7ea url('../images/smenubg.gif') left top repeat-x; color: #000; font-size: 90%; margin: 0 0 2em 0; } #sidebar .box h3 { color: #000; padding-bottom: 10px; } #sidebar .box .tr { background: transparent url('../images/smenucaptr.gif') right top no-repeat; } #sidebar .box .br { background: transparent url('../images/smenucapbr.gif') right bottom no-repeat; padding: 15px 10px 0 18px; } #sidebar .sidebar-nav { background: #ff7f00 url('../images/menubg.gif') left top repeat-y; font-size: 110%; } #sidebar .sidebar-nav .tr { background: transparent url('../images/menucaptr.gif') right top no-repeat; } #sidebar .sidebar-nav .br { background: transparent url('../images/menucapbr.gif') right bottom no-repeat; padding: 10px 0 10px 18px; } #sidebar .sidebar-nav h3 { color: #fff; padding: 0 0 .5em; } #sidebar ul li a, #sidebar ul li .act { display: block; color: #ffe5cc; padding: .8em 0; text-decoration: none; } #sidebar ul li a:hover, #sidebar ul li a:focus { color: #fff; text-decoration: underline; } #sidebar ul li .act { background: transparent url('../images/menuact.gif') right 50% no-repeat; color: #fff; margin-right: -11px; } #footer { display: block; padding: 20px 0 40px; text-align: center; } #footer p { font-size: 90%; padding: 0 0 5px 0; } #footer, #footer a { color: #cbdfef; text-decoration: none; } #footer .cp, #footer .cp a { color: #fff; } #footer .cp span { padding: 0 .25em; } #footer .cp a:hover, #footer .cp a:focus { text-decoration: underline; } lamson-1.0pre11/doc/lamsonproject.org/output/css/style_ie.css0000644000076500000240000000022111167314675023744 0ustar zedshawstaff#header, #nav, #sidebar, #sidebar * { zoom: 1; } #sidebar, #sidebar .act { position: relative; z-index: 1; } #sidebar .act { z-index: 30; }lamson-1.0pre11/doc/lamsonproject.org/output/docs/0000755000076500000240000000000011313464573021556 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/docs/bounce_detection.html0000644000076500000240000006030211243654674025763 0ustar zedshawstaff LamsonProject: Bounce Detection

Bounce Detection

Lamson supports intelligent bounce detection with its lamson.bounce module. The bounce handling is based on a probability that, depending on found headers, the message is a bounce. It then gives you a nice clean interface to check who it’s from, the originating SMTP server, the error messages, and any human readable messages.

How Bounces Actually Work

Figuring out how a bounce is actually handled is a bit difficult because the majority of the information available is written by people who know very little of SMTP server operations. When the average programmer thinks of handling a bounce, she usually has one of these concepts in mind:

  1. The message could not be delivered, so the remote SMTP server sent back a bounce message.
  2. The message could not be delivered, so the local SMTP server sent back a bounce message.
  3. The message could not be delivered, so the recipient’s email client sent back a bounce message.
  4. The message could not be delivered, so Lamson crafted a bounce message.

Obviously logically you can’t have the recipient’s email client send you a bounce unless the user does something weird (and incredibly annoying). This makes sense because the message was not delivered. How can the email client send back a bounce if they don’t receive the message.

Yes, some clients do support sending bounces, but very few people use this feature. If you do please talk about it as one more edge case to deal with.

Next Lamson can’t craft the bounce messages for you because Lamson is simply trying to deliver outgoing mail to a smart-host. Lamson does nothing but ask the smart-host (your local SMTP server) to deliver, and then waits for a response. That means Lamson is not sending you any kind of bounce unless you write code to make it do that.

That leaves only two options for who is really sending the bounce message: the remote MTA or the local MTA. The truth is it’s a little bit of both.

Your Local MTA Is A Person

How a bounce works in practice involves two SMTP servers: your local smart-host, and the remote server that it tries to connect to for delivery. The message you actually get in lamson is from the local server, and usually an address at that local server. It does not come from the remote server, but inside your bounce will be a message and status indicators from the remote server indicating why it bounced.

What happens is your local server attempts to deliver, and the remote server rejects it for some reason. Now your local server is supposed to try again in certain situations, but after a certain number of rejections or failures it crafts a bounce message. That bounce message is then returned to your lamson server based on the envelope header of the message (more on that later).

Now, what’s inside this message? Well, it’s a oddly nested barely standardized pile of random other messages. This is the frustration with bounce handling. You pretty much either have to use a probability mechanism (like Lamson does) or you have to craft your bounce handling for your very specific local server and any other possible servers you talk with. Even then you can have problems dealing with bounces reliably.

Inside this description is an important concept to understand:

Lamson does NOT process bounces from the remote MTA or the remote user in any way. Lamson process bounces from the local SMTP server.

This is important because if you try to use the bounce message as if it comes from the remote user, then you’ll accidentally put your local server into a state that prevents you from processing future bounce messages.

If you did not get that, reread this whole section again until moving forward.

“Hard” vs. “Soft”

Another complexity in dealing with bounces is the concept of “Hard” vs. “Soft” bounces. My opinion is that the distinction is fairly retarded since it is almost entirely unreliable and has no meaning to someone trying to handle a bounce.

In popular terminology the main difference is this:

  • Hard bounce means that person does not exist, so I can not sell him any more crap and need to remove him (maybe).
  • Soft bounce means that person still exists, but my marketing bullshit didn’t get through, I should try again 10 or 15 more times until he gets my important message about winning the lottery.

You may be laughing, or screaming various pedantic specifics, but the above two distinctions are both how many email services look at bounces, and how most malicious mail users view them.

For the mail services, leaking a hard or soft bounce is a security problem because it tells a malicious sender if that address is a valid person, why the sender was blocked, and how they can work around it. This is why many of the error messages you get back are vague and mostly lies. The major email services just don’t want to give you any information that you can use to circumvent their anti-spam measures.

The services that have the strictest anti-spam measures also have the most nasty disgusting spam on the web pages displaying user’s mail. Yahoo is both the worst for erroneously blocking email and for showing the worst most tricky spam all over every square inch of their mail service.

How does this relate to your Lamson application? It basically says that you should treat bounces as being basically soft bounces all the time, and then tune your rules heuristically over time as you run into more and more.

VERP

The final topic to touch on before getting into how Lamson handles bounces is one of VERP. Remember in the description of bounce handling above I said that your local MTA sends the bounce message back to the envelope from not the From in the headers (well, most ones will). Because of this you can have a From in the envelope that is only replied to when there’s a bounce, and then put the real From in the headers for normal processing.

This is effectively what Variable Envelope Return Path does to process bounce messages. Rather than attempt to parse the body of a bounce message, VERP rewrite the From address so that when a bounce is returned, you only have to process the returned address to see what the original was.

Lamson could potentially support this, but it has several problems for generic bounce handling which meant that actually parsing bounce bodies was a better option.

Using Lamson’s Bounce Handling

Using Lamson’s bounce handling is fairly simple. It involves the following process:

  1. Import a special decorator bounce_to from lamson.bounce
  2. Create two (or one) handlers to deal with bounces.
  3. Place the decorator on any START entry points to your handlers that can receive bounces, pointing them at your handlers.
  4. Handle the bounces in your handlers, making sure to return back to the START state for the local MTA (remember, the local MTA is a person for bounce handling).

Here’s some simple code that does exactly this by just ignoring bounces from the myinboxisnota.tv example:

from config.settings import BOUNCES
from lamson.routing import route
from lamson.bounce import bounce_to

@route(".+")
def IGNORE_BOUNCE(message):
    bounces = queue.Queue(BOUNCES)
    bounces.push(message)
    return START

@route("start@(host)")
@bounce_to(soft=IGNORE_BOUNCE, hard=IGNORE_BOUNCE)
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING

This example is stripped down from the real code so you can see what’s going on. If we walk through this you can see what happens:

  1. First we import the BOUNCES variable from config.settings which is just the queue we want to dump bounces into.
  2. We then create a special handler named IGNORE_BOUNCE that accepts a message to anything in its route and just puts the message in the BOUNCES queue.
  3. This IGNORE_BOUNCE handler then immediately returns START so we go back to the START state.
  4. On the START state you’ll see that we have our bounce_to declaration that points for hard and soft bounces at IGNORE_BOUNCE.
  5. This decorator wraps your handler in a little check that, if the message is a bounce, your START state won’t get called, and instead your IGNORE_BOUNCE state will.

That is pretty much all you need to deal with to re-route bounces somewhere else. You can also redirect them to a totally different handler, which is exactly what the librelist.com example does.

How It Works

How does Lamson figure out that something is a bounce? What Lamson does is it assumes bounces will have some or all of these headers:

BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}

The problem traditionally with parsing a bounce message was that, while you need to find all of these headers, there was no real standard as to how the messages in the bounce message are structured. From my postfix server the bounce message is a sequence of about 6 nested attachments containing other messages, and sometimes the nesting goes three deep.

Rather than rely on this structure (which changes all the time) or that these headers are always present (they aren’t), Lamson takes a probabilistic approach based on the number of headers and properly formatted values it finds in all nested attachments. The process goes something like this:

  1. Traverse all the possible nested attachments.
  2. Try to find each header in the attachment. If it’s found add a point.
  3. If the header is found, use the regex associated with it (above) to try to match the value.
    1. If the value matches, then keep the regex captures for later. Add another point.
  4. For each header found, and any regex captures that matched the bodies, put them into an internal dict for analyzing the bounce information.
  5. Finally, calculate a probability score that is the total number of BOUNCE_MATCHERS * 2.0 / points.

In general, if a message is found that has a 0.3 or higher bounce probability then it is considered a bounce message and you can look at it. The bounce_to decorator has a threshold you can adjust if you want to be more or less strict.

The final result of this processing (which is actually very fast) is that any calls to MailRequest.is_bounce will either return True or False, and then after you call is_bounce you can access the MailRequest.bounce attribute to analyze the information. That information is captured and cooked into a BounceAnalyzer object.

What It Looks Like To Receive One

It’s also instructive to see what it looks like when Lamson processes a bounce message. Here’s the librelist.com server processing a bounce message:

2009-08-21 13:43:47,223 - root - DEBUG - Pulled message with key:
'1250876622.V8e00I219de0M128371' off

2009-08-21 13:43:47,231 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From: u'"SPAMMER"
', to To [u'lamson@librelist.com'].

2009-08-21 13:43:47,251 - routing - DEBUG - Matched u'lamson@librelist.com'
against START.

2009-08-21 13:43:47,332 - routing - DEBUG - Message to
set([u'lamson@librelist.com']) was handled by app.handlers.admin.START

2009-08-21 13:43:57,367 - root - DEBUG - Pulled message with key:
'1250876627.V8e00I219661M719350' off

2009-08-21 13:43:57,381 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From:
u'MAILER-DAEMON@librelist.com (Mail Delivery System)', to To
[u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com'].

2009-08-21 13:43:57,410 - routing - DEBUG - Matched
u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com' against START.

2009-08-21 13:43:57,431 - routing - DEBUG - Message to
set([u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com']) was
handled by app.handlers.admin.START

These log messages show the following interaction:

  1. SPAMMER@hotmail.com tried to spam the lamson@librelist.com mailing list.
  2. They were required to subscribe, so Lamson sent them a confirmation mail.
  3. That message bounced, so Postfix sent back a bounce message from MAILER-DAEMON@librelist.com to Lamson.
  4. This message from MAILER-DAEMON is a bounce, so the librelist code handled it on the START state, NOT the CONFIRMING_SUBSCRIBE state.
  5. Internally, librelist looked up the target user and just zapped them.

That shows how the bounce doesn’t come from SPAMMER@hotmail.com nor any server at hotmail.com, but instead from MAILER-DAEMON@librelist.com. You could also get messages from a remote MTA, but if they were found to be bounce messages then that remote MTA would be treated like your own MTA.

Gettting Bounce Information From BounceAnalyzer

The BounceAnalyzer does the work of figuring out additional useful information you can use to determine what to do with the bounce. It looks at the final headers that are scanned in the above process and pulls out important information everyone needs. The list of information you can get is:

  • primary_status — The main status number that determines hard vs soft.
  • secondary_status — Advice status.
  • combined_status — the 2nd and 3rd number combined gives more detail.
  • remote_mta — The MTA that you sent mail to and aborted.
  • reporting_mta — The MTA that was sending the mail and has to report to you.
  • diagnostic_codes — Human readable codes usually with info from the provider.
  • action — Usually 'failed’, and turns out to be not too useful.
  • content_parts — All the attachments found as a hash keyed by the type.
  • original — The original message, if it’s found.
  • report — All report elements, as lamson.encoding.MailBase raw messages.
  • notification — Usually the detailed reason you bounced.

But, refer to the documentation for more accurate listings. An important feature is that the status codes are parsed and converted into a standard list available in lamson.bounce based on their numeric values. Rather than parse the details given by the remote MTA, you just use the number codes to get a human readable output.

The best way to see all that’s possible is to take a glance at the Lamson unit test for the BounceAnalyzer:

def test_bounce_analyzer_on_bounce():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
    assert_equal(bm.bounce.primary_status, (5, u'Permanent Failure'))
    assert_equal(bm.bounce.secondary_status, (1, u'Addressing Status'))
    assert_equal(bm.bounce.combined_status, (11, u'Bad destination mailbox address'))

    assert bm.bounce.is_hard()
    assert_equal(bm.bounce.is_hard(), not bm.bounce.is_soft())

    assert_equal(bm.bounce.remote_mta, u'gmail-smtp-in.l.google.com')
    assert_equal(bm.bounce.reporting_mta, u'mail.zedshaw.com')
    assert_equal(bm.bounce.final_recipient,
                 u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')
    assert_equal(bm.bounce.diagnostic_codes[0], u'550-5.1.1')
    assert_equal(bm.bounce.action, 'failed')
    assert 'Content-Description-Parts' in bm.bounce.headers

    assert bm.bounce.error_for_humans()

Here you can see that you can figure out if the bounce is hard vs. soft, get a human description, access status codes of various flavors, get the remote MTA’s name, the reporting MTA (your local), who it was originally for (final_recipient), and even access the raw bounce.headers if that’s not good enough.

Augmenting The Matchers

Another advantage of this method of processing the bounces is that if your SMTP server crafts something that hasn’t been handled, then you can augment the matchers being used. Simply update the lamson.bounce.BOUNCE_MATCHERS dict with your new ones and make sure to update lamson.bounce.BOUNCE_MAX to be 2 times that.

The status codes are also available in the same way. Refer to the source for more information.

One tricky part of Lamson’s bounce handling is that it does assume a certain structure for the BounceAnalyzer to get at the internal details. This structure is the one used by Postfix, and it should be the same for other servers. However, if you run into a structural difference, report the results back so the handling can be improved.

A More Complete Example

Finally, the librelist.com example code has a much more complete example of using bounces to disable users and shift their state. Rather than describe it in detail here, I’ll simply point you at the source releases so you can see it for yourself. Look in examples/librelist/app/handlers/bounce.py to see how it all works.

In fact, studying how this is triggered from the rest of the librelist example is a great way to learn how to use Lamson in an advanced fashion. Study well.

Conclusion

Lamson bounce handling is very advanced and can deal with a wide range of scenarios. It should work with a wide range of bounce styles and other servers, but feel free to report your own experiences and differences.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/bounce_detection.txt0000644000076500000240000004355411243654672025646 0ustar zedshawstaffTitle: Bounce Detection Lamson supports intelligent bounce detection with its "lamson.bounce":http://lamsonproject.org/docs/api/lamson.bounce-module.html module. The bounce handling is based on a probability that, depending on found headers, the message is a bounce. It then gives you a nice clean interface to check who it's from, the originating SMTP server, the error messages, and any human readable messages. h1. How Bounces Actually Work Figuring out how a bounce is actually handled is a bit difficult because the majority of the information available is written by people who know very little of SMTP server operations. When the average programmer thinks of handling a bounce, she usually has one of these concepts in mind: # The message could not be delivered, so the remote SMTP server sent back a bounce message. # The message could not be delivered, so the local SMTP server sent back a bounce message. # The message could not be delivered, so the recipient's email client sent back a bounce message. # The message could not be delivered, so Lamson crafted a bounce message. Obviously logically you can't have the recipient's email client send you a bounce unless the user does something weird (and incredibly annoying). This makes sense because the message was @not delivered@. How can the email client send back a bounce if they don't receive the message. bq. Yes, some clients do support sending bounces, but very few people use this feature. If you do please talk about it as one more edge case to deal with. Next Lamson can't craft the bounce messages for you because Lamson is simply trying to deliver outgoing mail to a smart-host. Lamson does nothing but ask the smart-host (your local SMTP server) to deliver, and then waits for a response. That means Lamson is not sending you any kind of bounce unless you write code to make it do that. That leaves only two options for @who@ is really sending the bounce message: the remote MTA or the local MTA. The truth is it's a little bit of both. h2. Your Local MTA Is A Person How a bounce works in practice involves two SMTP servers: your local smart-host, and the remote server that it tries to connect to for delivery. The message you actually get in lamson is from the @local@ server, and usually an address @at@ that local server. It does not come from the remote server, but inside your bounce will be a message and status indicators from the remote server indicating why it bounced. What happens is your local server attempts to deliver, and the remote server rejects it for some reason. Now your local server is supposed to try again in certain situations, but after a certain number of rejections or failures it crafts a bounce message. That bounce message is then returned to your @lamson@ server based on the @envelope@ header of the message (more on that later). Now, what's inside this message? Well, it's a oddly nested barely standardized pile of random other messages. This is the frustration with bounce handling. You pretty much either have to use a probability mechanism (like Lamson does) or you have to craft your bounce handling for your very specific local server and any other possible servers you talk with. Even then you can have problems dealing with bounces reliably. Inside this description is an important concept to understand: bq. Lamson does NOT process bounces from the remote MTA or the remote user in any way. Lamson process bounces from the @local@ SMTP server. This is important because if you try to use the bounce message as if it comes from the remote user, then you'll accidentally put your @local@ server into a state that prevents you from processing future bounce messages. If you did not get that, reread this whole section again until moving forward. h2. "Hard" vs. "Soft" Another complexity in dealing with bounces is the concept of "Hard" vs. "Soft" bounces. My opinion is that the distinction is fairly retarded since it is almost entirely unreliable and has no meaning to someone trying to handle a bounce. In popular terminology the main difference is this: * Hard bounce means that person does not exist, so I can not sell him any more crap and need to remove him (maybe). * Soft bounce means that person still exists, but my marketing bullshit didn't get through, I should try again 10 or 15 more times until he gets my important message about winning the lottery. You may be laughing, or screaming various pedantic specifics, but the above two distinctions are both how many email services look at bounces, and how most malicious mail users view them. For the mail services, leaking a hard or soft bounce is a security problem because it tells a malicious sender if that address is a valid person, why the sender was blocked, and how they can work around it. This is why many of the error messages you get back are vague and mostly lies. The major email services just don't want to give you any information that you can use to circumvent their anti-spam measures. bq. The services that have the strictest anti-spam measures also have the most nasty disgusting spam on the web pages displaying user's mail. Yahoo is both the worst for erroneously blocking email and for showing the worst most tricky spam all over every square inch of their mail service. How does this relate to your Lamson application? It basically says that you should treat bounces as being basically soft bounces all the time, and then tune your rules heuristically over time as you run into more and more. h2. VERP The final topic to touch on before getting into how Lamson handles bounces is one of VERP. Remember in the description of bounce handling above I said that your @local@ MTA sends the bounce message back to the @envelope from@ not the From in the headers (well, most ones will). Because of this you can have a From in the envelope that is only replied to when there's a bounce, and then put the real From in the headers for normal processing. This is effectively what "Variable Envelope Return Path":http://en.wikipedia.org/wiki/Variable_envelope_return_path does to process bounce messages. Rather than attempt to parse the body of a bounce message, VERP rewrite the From address so that when a bounce is returned, you only have to process the returned address to see what the original was. Lamson could potentially support this, but it has several problems for generic bounce handling which meant that actually parsing bounce bodies was a better option. h2. Using Lamson's Bounce Handling Using Lamson's bounce handling is fairly simple. It involves the following process: # Import a special decorator @bounce_to@ from "lamson.bounce":http://lamsonproject.org/docs/api/lamson.bounce-module.html # Create two (or one) handlers to deal with bounces. # Place the decorator on any @START@ entry points to your handlers that can receive bounces, pointing them at your handlers. # Handle the bounces in your handlers, making sure to return back to the @START@ state for the local MTA (remember, the local MTA is a person for bounce handling). Here's some simple code that does exactly this by just ignoring bounces from the "myinboxisnota.tv":http://myinboxisnota.tv/ example:
from config.settings import BOUNCES
from lamson.routing import route
from lamson.bounce import bounce_to

@route(".+")
def IGNORE_BOUNCE(message):
    bounces = queue.Queue(BOUNCES)
    bounces.push(message)
    return START

@route("start@(host)")
@bounce_to(soft=IGNORE_BOUNCE, hard=IGNORE_BOUNCE)
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING
This example is stripped down from the real code so you can see what's going on. If we walk through this you can see what happens: # First we import the @BOUNCES@ variable from @config.settings@ which is just the queue we want to dump bounces into. # We then create a special handler named @IGNORE_BOUNCE@ that accepts a message to anything in its @route@ and just puts the message in the @BOUNCES@ queue. # This @IGNORE_BOUNCE@ handler then immediately returns @START@ so we go back to the START state. # On the @START@ state you'll see that we have our @bounce_to@ declaration that points for @hard@ and @soft@ bounces at @IGNORE_BOUNCE@. # This decorator wraps your handler in a little check that, if the message is a bounce, your @START@ state won't get called, and instead your @IGNORE_BOUNCE@ state will. That is pretty much all you need to deal with to re-route bounces somewhere else. You can also redirect them to a totally different handler, which is exactly what the "librelist.com":http://librelist.com/ example does. h2. How It Works How does Lamson figure out that something is a bounce? What Lamson does is it assumes bounces will have some or all of these headers:
BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}
The problem traditionally with parsing a bounce message was that, while you need to find all of these headers, there was no real standard as to how the messages in the bounce message are structured. From my "postfix":http://www.postfix.org/ server the bounce message is a sequence of about 6 nested attachments containing other messages, and sometimes the nesting goes three deep. Rather than rely on this structure (which changes all the time) or that these headers are always present (they aren't), Lamson takes a probabilistic approach based on the number of headers and properly formatted values it finds in @all@ nested attachments. The process goes something like this: # Traverse all the possible nested attachments. # Try to find each header in the attachment. If it's found add a point. # If the header is found, use the regex associated with it (above) to try to match the value. ## If the value matches, then keep the regex captures for later. Add another point. # For each header found, and any regex captures that matched the bodies, put them into an internal dict for analyzing the bounce information. # Finally, calculate a probability score that is the total number of BOUNCE_MATCHERS * 2.0 / points. In general, if a message is found that has a 0.3 or higher bounce probability then it is considered a bounce message and you can look at it. The @bounce_to@ decorator has a threshold you can adjust if you want to be more or less strict. The final result of this processing (which is actually very fast) is that any calls to @MailRequest.is_bounce@ will either return True or False, and then after you call is_bounce you can access the @MailRequest.bounce@ attribute to analyze the information. That information is captured and cooked into a "BounceAnalyzer":http://lamsonproject.org/docs/api/lamson.bounce.BounceAnalyzer-class.html object. h2. What It Looks Like To Receive One It's also instructive to see what it looks like when Lamson processes a bounce message. Here's the "librelist.com":http://librelist.com/ server processing a bounce message:
2009-08-21 13:43:47,223 - root - DEBUG - Pulled message with key:
'1250876622.V8e00I219de0M128371' off

2009-08-21 13:43:47,231 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From: u'"SPAMMER"
', to To [u'lamson@librelist.com'].

2009-08-21 13:43:47,251 - routing - DEBUG - Matched u'lamson@librelist.com'
against START.

2009-08-21 13:43:47,332 - routing - DEBUG - Message to
set([u'lamson@librelist.com']) was handled by app.handlers.admin.START

2009-08-21 13:43:57,367 - root - DEBUG - Pulled message with key:
'1250876627.V8e00I219661M719350' off

2009-08-21 13:43:57,381 - root - DEBUG - Message received from Peer:
'/var/mail/vhosts/librelist.com/delivery/', From:
u'MAILER-DAEMON@librelist.com (Mail Delivery System)', to To
[u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com'].

2009-08-21 13:43:57,410 - routing - DEBUG - Matched
u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com' against START.

2009-08-21 13:43:57,431 - routing - DEBUG - Message to
set([u'lamson-confirm-74e2ca94b24a4be18da277f4666a6494@librelist.com']) was
handled by app.handlers.admin.START
These log messages show the following interaction: # SPAMMER@hotmail.com tried to spam the lamson@librelist.com mailing list. # They were required to subscribe, so Lamson sent them a confirmation mail. # That message bounced, so Postfix sent back a bounce message from MAILER-DAEMON@librelist.com to Lamson. # This message from MAILER-DAEMON is a bounce, so the librelist code handled it on the START state, NOT the CONFIRMING_SUBSCRIBE state. # Internally, librelist looked up the target user and just zapped them. That shows how the bounce doesn't come from SPAMMER@hotmail.com nor any server at hotmail.com, but instead from MAILER-DAEMON@librelist.com. You could also get messages from a remote MTA, but if they were found to be bounce messages then that remote MTA would be treated like your own MTA. h2. Gettting Bounce Information From BounceAnalyzer The "BounceAnalyzer":http://lamsonproject.org/docs/api/lamson.bounce.BounceAnalyzer-class.html does the work of figuring out additional useful information you can use to determine what to do with the bounce. It looks at the final headers that are scanned in the above process and pulls out important information everyone needs. The list of information you can get is: * primary_status -- The main status number that determines hard vs soft. * secondary_status -- Advice status. * combined_status -- the 2nd and 3rd number combined gives more detail. * remote_mta -- The MTA that you sent mail to and aborted. * reporting_mta -- The MTA that was sending the mail and has to report to you. * diagnostic_codes -- Human readable codes usually with info from the provider. * action -- Usually 'failed', and turns out to be not too useful. * content_parts -- All the attachments found as a hash keyed by the type. * original -- The original message, if it's found. * report -- All report elements, as lamson.encoding.MailBase raw messages. * notification -- Usually the detailed reason you bounced. But, refer to the documentation for more accurate listings. An important feature is that the status codes are parsed and converted into a standard list available in @lamson.bounce@ based on their numeric values. Rather than parse the details given by the remote MTA, you just use the number codes to get a human readable output. The best way to see all that's possible is to take a glance at the Lamson unit test for the BounceAnalyzer:
def test_bounce_analyzer_on_bounce():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
    assert_equal(bm.bounce.primary_status, (5, u'Permanent Failure'))
    assert_equal(bm.bounce.secondary_status, (1, u'Addressing Status'))
    assert_equal(bm.bounce.combined_status, (11, u'Bad destination mailbox address'))

    assert bm.bounce.is_hard()
    assert_equal(bm.bounce.is_hard(), not bm.bounce.is_soft())

    assert_equal(bm.bounce.remote_mta, u'gmail-smtp-in.l.google.com')
    assert_equal(bm.bounce.reporting_mta, u'mail.zedshaw.com')
    assert_equal(bm.bounce.final_recipient,
                 u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')
    assert_equal(bm.bounce.diagnostic_codes[0], u'550-5.1.1')
    assert_equal(bm.bounce.action, 'failed')
    assert 'Content-Description-Parts' in bm.bounce.headers

    assert bm.bounce.error_for_humans()
Here you can see that you can figure out if the bounce is hard vs. soft, get a human description, access status codes of various flavors, get the remote MTA's name, the reporting MTA (your local), who it was originally for (final_recipient), and even access the raw @bounce.headers@ if that's not good enough. h2. Augmenting The Matchers Another advantage of this method of processing the bounces is that if your SMTP server crafts something that hasn't been handled, then you can augment the matchers being used. Simply update the @lamson.bounce.BOUNCE_MATCHERS@ dict with your new ones and make sure to update @lamson.bounce.BOUNCE_MAX@ to be 2 times that. The status codes are also available in the same way. Refer to the source for more information. One tricky part of Lamson's bounce handling is that it does assume a certain structure for the BounceAnalyzer to get at the internal details. This structure is the one used by Postfix, and it should be the same for other servers. However, if you run into a structural difference, report the results back so the handling can be improved. h2. A More Complete Example Finally, the "librelist.com":http://librelist.com/ example code has a much more complete example of using bounces to disable users and shift their state. Rather than describe it in detail here, I'll simply point you at the "source releases":/releases/ so you can see it for yourself. Look in @examples/librelist/app/handlers/bounce.py@ to see how it all works. In fact, studying how this is triggered from the rest of the librelist example is a great way to learn how to use Lamson in an advanced fashion. Study well. h2. Conclusion Lamson bounce handling is very advanced and can deal with a wide range of scenarios. It should work with a wide range of bounce styles and other servers, but feel free to report your own experiences and differences. lamson-1.0pre11/doc/lamsonproject.org/output/docs/confirmations.html0000644000076500000240000004525111310760166025320 0ustar zedshawstaff LamsonProject: Confirmations

Confirmations

You never know who’s going to send an email to your Lamson application. It may be a real person, or a spam bot. Currently most of the spam bots aren’t very intelligent (probably because they haven’t discovered Lamson yet) so a simple easy way to weed out the randomness is to confirm people at important “choke points” in your application.

Lamson provides a simple API in lamson.confirm to help you send out and verify confirmation emails. You provide a storage class for keeping track of who you’re expecting to confirm, a template for your message to send, and it does the rest. It’s even advanced enough to keep the original email around so you can resend it after the confirmation.

The General Theory

The confirmation API assumes a few things about how you’re doing your confirmations:

  1. You want them to reply to an email to confirm.
  2. The address they reply to confirm has a “target” to differentiate it from other parts of your application that needs confirmation.
  3. The address they reply to also has a hex formatted randomly generated UUID for security, and your storage can handle that length.
  4. You want to store the message they sent so you can get it back after they confirm.

With those fairly reasonable assumptions you only need to then setup the confirmation API in your config/settings.py file and use it in your handlers, preferrably at your START state and anywhere else that you need to validate the user before they do something destructive.

Simplest Usage

The most basic usage of the lamson.confirm API is to put it in your config/settings.py and then access it in your handlers. You configure it like this:

from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage()
CONFIRM = confirm.ConfirmationEngine('run/pending')

This puts a variable in your settings.py that you can access from your handlers to craft and verify the confirmation messages.

By default uses a simple in-memory dict to store the confirmations. That’s fine for testing and an initial deploy, but you’ll probably want to switch to a permanent form of storage for later. Further on in this document you’ll see how.

Next, you need to use it in your START state, and then in a CONFIRMING state. Here’s a simple example:

from config.settings import relay, CONFIRM

@route("start@(host)")
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING

@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('start', message['from'], id_number)

    if original:
        welcome = view.respond(locals(), "mail/welcome.msg", 
                           From='noreply@%(host)s',
                           To=message['from'],
                           Subject="Welcome")
        relay.deliver(welcome)

        return PROTECTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING

Here’s how the above code works:

  1. First we import the CONFIRM variable so we can use it.
  2. In our START handler (which is accepts start@(host)) we use the API to send out the confirmation message they should reply-to. Notice how we give a “start” target as the second argument, this is important.
  3. Then we transition to CONFIRMING and wait for them to reply to that message.
  4. The user then replies to the message we sent, so we handle the CONFIRMING state. Notice that we are handling “start-confirm-(id_number)” as the initial message, with “start” being the target (2nd parameter) from our above CONFIRM.send call.
  5. In CONFIRMING we use the CONFRIM.verify method to validate that it’s from the right person, to the right target (“start”) and that they got the secret (id_number) right.
  6. Finally, if it’s right we send them a welcome message, and if it’s not we just ignore the message.

An alternative to ignoring the failed confirmation from them is to cancel it and go back to the START state for them. The danger with that method though is a spam bot will get into a loop where you are sending them constant confirmation messages in a loop between START and CONFIRMING. It’s best to drop it, and maybe provide a “cancel” mechanism.

Using Shelf Storage

Other than a few other methods, there’s only a need to change the storage. The simplest change is to provide a dict-like interface to the lamson.confirm.ConfirmationEngine to store. Easiest available is the Python shelf module which gives a dict interface to various key/value storage backends.

To use one, just change your code in config/settings.py to be like this:

import shelve
from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage(db=shelve.open("run/confirmationsdb"))
CONFIRM = confirm.ConfirmationEngine('run/pending', CONFIRM_STORAGE)

All you do is create a lamson.confirm.ConfirmationStorage and give the db= parameter a dict it can use. Everything else will be the same.

There might be thread issues with this, and it will definitely fail if you use mulitple processes. See the next section on using a Django Model.

Using A Django ORM Model

In the librelist.com example code you’ll find that it stores the confirmations in the Django model in the webapp/librelist directory. This is actually easily setup, so first read Hooking Into Django to learn how to access a Django ORM. After that, you write a simple version of ConfirmationStorage that would look something like this:

from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()

This is from Librelist, so you see we just import the Confirmation model and then wrap it with the get, delete, set, and clear methods that ConfirmationEngine needs to run.

For completeness, here’s what the Django Confirmation model looks like:

class Confirmation(models.Model):
    from_address = models.EmailField()
    request_date = models.DateTimeField(auto_now_add=True)
    expected_secret = models.CharField(max_length=50)
    pending_message_id = models.CharField(max_length=200)
    list_name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.from_address

Final step is to configure it in your config/settings.py thusly:

from lamson import confirm

...

from app.model.confirmation import DjangoConfirmStorage
CONFIRM = confirm.ConfirmationEngine('run/pending', DjangoConfirmStorage())

That’s all there is to it. This is actually a nice setup because you can use the Django Admin to manage it during your first deployments.

Other ORM

For other ORM systems simply use the same pattern as the Django example above. You just create a similar model, wrap it with your own version of ConfirmationStorage and plug it into the ConfirmationEngine you use.

Targets

The only other thing to understand is why the API has a “target” parameter. Let’s look at the call to CONFIRM.send again:

CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())

The “start” string as the second parameter acts as a the target. It says that this user needs to confirm for the “start” target when they do their reply. That’s why the route on CONFIRMING is then like this:

@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')

You could also make the above a pattern, for example in the Librelist confirmations we’re confirming that the user is joining a certain mailing list:

@route('(list_name)-confirm-(id_number)@(host)')
def CONFIRMING_SUBSCRIBE(message, list_name=None, id_number=None, host=None):
    original = CONFIRM.verify(list_name, message['from'], id_number)
    ...

This way, the user could have multiple simultaneous confirmations going for different lists and they won’t step on eachother.

Without this differentiator, you’d have to either restrict users to just one confirmation at a time, or you’d end up getting the data all confused.

Confirming Off A Web Link

If you want people to go to a web link instead of simply replying, then you have to do the following:

  1. Either write your own version, or subclass ConfirmationEngine so that it uses an address they can’t reply to.
  2. Make sure you use an ORM that can access your database and store both the confirmation info, and each users’s state.
  3. When the user hits the link you give them and does whatever you need, use the web framework’s ORM to validate their confirmation.
  4. Once your web framework has validated their confirmation, then change their state in Lamson’s state using your web framework ORM out of CONFIRMING and into the next state.

Assuming you’re doing this all with Python it should be fairly trivial.

Confirm Only By Web Is Bad

I would advise against this method though, since it doesn’t really confirm that the email address you received worked. One of the purposes of doing a confirmation email exchange is to make sure that this person can both send and receive. If you have to point them at your web site, consider having this process instead:

  1. Their first interaction with your service sends out an email that sends them to a web page, and transitions them to a CONFIRMING state but do not send them a confirmation reply address from Lamson. You’ll actually “delay” this until they fill out your web forms.
  2. In your web framework, you have them fill out forms and such, and then send them the real confirmation message using Lamson. Since the Confirmation API is Python you could do this directly in any Python web framework. You’re basically moving the call to CONFRIM.send from your START handler into the web framework.
  3. Then your web framework will have sent them a real confirmation email, not just a link, so when they reply, continue with the usual Lamson confirmation process described above.

Doing it this way ends up being a good balance between too many clicks and replies, but too few to confirm that the end user can actually reply to email you send them.

The Pending Queue

You should also notice in the above examples that the original message is stored in a “pending queue” and then given back to you later. This is handy for either finishing their original request without further intervention, or inspecting what they original wanted to do. In the original Librelist code I would take their first message, confirm them, and then pull it out of the pending queue to send it on. This turned out to not work because socially people “subscribe” with a garbage first message, but technically it worked great.

You may want to periodically go through this queue and purge any messages that aren’t found in the ConfirmationsStorage. Probably with a simple Python script and a cronjob.

Conclusion

The Lamson confirmation API encapsulates a pattern for confirming potential users. Feel free to suggest improvements to the API if you find further patterns that are needed.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/confirmations.txt0000644000076500000240000003057611310760160025171 0ustar zedshawstaffTitle: Confirmations You never know who's going to send an email to your Lamson application. It may be a real person, or a spam bot. Currently most of the spam bots aren't very intelligent (probably because they haven't discovered Lamson yet) so a simple easy way to weed out the randomness is to confirm people at important "choke points" in your application. Lamson provides a simple API in "lamson.confirm":http://lamsonproject.org/docs/api/lamson.confirm-module.html to help you send out and verify confirmation emails. You provide a storage class for keeping track of who you're expecting to confirm, a template for your message to send, and it does the rest. It's even advanced enough to keep the original email around so you can resend it after the confirmation. h2. The General Theory The confirmation API assumes a few things about how you're doing your confirmations: # You want them to reply to an email to confirm. # The address they reply to confirm has a "target" to differentiate it from other parts of your application that needs confirmation. # The address they reply to also has a hex formatted randomly generated UUID for security, and your storage can handle that length. # You want to store the message they sent so you can get it back after they confirm. With those fairly reasonable assumptions you only need to then setup the confirmation API in your @config/settings.py@ file and use it in your handlers, preferrably at your @START@ state and anywhere else that you need to validate the user before they do something destructive. h2. Simplest Usage The most basic usage of the "lamson.confirm":http://lamsonproject.org/docs/api/lamson.confirm-module.html API is to put it in your @config/settings.py@ and then access it in your handlers. You configure it like this:
from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage()
CONFIRM = confirm.ConfirmationEngine('run/pending')
This puts a variable in your settings.py that you can access from your handlers to craft and verify the confirmation messages. bq. By default uses a simple in-memory dict to store the confirmations. That's fine for testing and an initial deploy, but you'll probably want to switch to a permanent form of storage for later. Further on in this document you'll see how. Next, you need to use it in your @START@ state, and then in a @CONFIRMING@ state. Here's a simple example:
from config.settings import relay, CONFIRM

@route("start@(host)")
def START(message, host=None):
    CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
    return CONFIRMING

@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('start', message['from'], id_number)

    if original:
        welcome = view.respond(locals(), "mail/welcome.msg", 
                           From='noreply@%(host)s',
                           To=message['from'],
                           Subject="Welcome")
        relay.deliver(welcome)

        return PROTECTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING
Here's how the above code works: # First we import the CONFIRM variable so we can use it. # In our @START@ handler (which is accepts start@(host)) we use the API to send out the confirmation message they should reply-to. Notice how we give a "start" target as the second argument, this is important. # Then we transition to CONFIRMING and wait for them to reply to that message. # The user then replies to the message we sent, so we handle the CONFIRMING state. Notice that we are handling "start-confirm-(id_number)" as the initial message, with "start" being the target (2nd parameter) from our above @CONFIRM.send@ call. # In CONFIRMING we use the @CONFRIM.verify@ method to validate that it's from the right person, to the right target ("start") and that they got the secret (id_number) right. # Finally, if it's right we send them a welcome message, and if it's not we just ignore the message. An alternative to ignoring the failed confirmation from them is to cancel it and go back to the START state for them. The danger with that method though is a spam bot will get into a loop where you are sending them constant confirmation messages in a loop between START and CONFIRMING. It's best to drop it, and maybe provide a "cancel" mechanism. h2. Using Shelf Storage Other than a few other methods, there's only a need to change the storage. The simplest change is to provide a dict-like interface to the "lamson.confirm.ConfirmationEngine":http://lamsonproject.org/docs/api/lamson.confirm.ConfirmationEngine-class.html to store. Easiest available is the Python "shelf":http://docs.python.org/library/shelve.html module which gives a dict interface to various key/value storage backends. To use one, just change your code in @config/settings.py@ to be like this:
import shelve
from lamson import confirm

...
CONFIRM_STORAGE=confirm.ConfirmationStorage(db=shelve.open("run/confirmationsdb"))
CONFIRM = confirm.ConfirmationEngine('run/pending', CONFIRM_STORAGE)
All you do is create a "lamson.confirm.ConfirmationStorage":http://lamsonproject.org/docs/api/lamson.confirm.ConfirmationStorage-class.html and give the @db=@ parameter a dict it can use. Everything else will be the same. bq. There might be thread issues with this, and it will definitely fail if you use mulitple processes. See the next section on using a Django Model. h2. Using A Django ORM Model In the "librelist.com":http://librelist.com/ example code you'll find that it stores the confirmations in the Django model in the @webapp/librelist@ directory. This is actually easily setup, so first read "Hooking Into Django":/docs/hooking_into_django.html to learn how to access a Django ORM. After that, you write a simple version of @ConfirmationStorage@ that would look something like this:
from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()
This is from Librelist, so you see we just import the @Confirmation@ model and then wrap it with the @get@, @delete@, @set@, and @clear@ methods that @ConfirmationEngine@ needs to run. For completeness, here's what the Django @Confirmation@ model looks like:
class Confirmation(models.Model):
    from_address = models.EmailField()
    request_date = models.DateTimeField(auto_now_add=True)
    expected_secret = models.CharField(max_length=50)
    pending_message_id = models.CharField(max_length=200)
    list_name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.from_address
Final step is to configure it in your @config/settings.py@ thusly:
from lamson import confirm

...

from app.model.confirmation import DjangoConfirmStorage
CONFIRM = confirm.ConfirmationEngine('run/pending', DjangoConfirmStorage())
That's all there is to it. This is actually a nice setup because you can use the Django Admin to manage it during your first deployments. h2. Other ORM For other ORM systems simply use the same pattern as the Django example above. You just create a similar model, wrap it with your own version of @ConfirmationStorage@ and plug it into the @ConfirmationEngine@ you use. h2. Targets The only other thing to understand is why the API has a "target" parameter. Let's look at the call to CONFIRM.send again:
CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
The "start" string as the second parameter acts as a the target. It says that this user needs to confirm for the "start" target when they do their reply. That's why the @route@ on @CONFIRMING@ is then like this:
@route("start-confirm-(id_number)@(host)", id_number='[a-z0-9]+')
You could also make the above a pattern, for example in the Librelist confirmations we're confirming that the user is joining a certain mailing list:
@route('(list_name)-confirm-(id_number)@(host)')
def CONFIRMING_SUBSCRIBE(message, list_name=None, id_number=None, host=None):
    original = CONFIRM.verify(list_name, message['from'], id_number)
    ...
This way, the user could have multiple simultaneous confirmations going for different lists and they won't step on eachother. Without this differentiator, you'd have to either restrict users to just one confirmation at a time, or you'd end up getting the data all confused. h2. Confirming Off A Web Link If you want people to go to a web link instead of simply replying, then you have to do the following: # Either write your own version, or subclass @ConfirmationEngine@ so that it uses an address they can't reply to. # Make sure you use an ORM that can access your database and store both the confirmation info, and each users's state. # When the user hits the link you give them and does whatever you need, use the web framework's ORM to validate their confirmation. # Once your web framework has validated their confirmation, then change their state *in Lamson's state* using *your web framework ORM* out of CONFIRMING and into the next state. Assuming you're doing this all with Python it should be fairly trivial. h2. Confirm Only By Web Is Bad I would advise against this method though, since it doesn't really confirm that the email address you received worked. One of the purposes of doing a confirmation email exchange is to make sure that this person can both *send* and *receive*. If you have to point them at your web site, consider having this process instead: # Their first interaction with your service sends out an email that sends them to a web page, and transitions them to a @CONFIRMING@ state *but do not send them a confirmation reply address from Lamson.* You'll actually "delay" this until they fill out your web forms. # In your web framework, you have them fill out forms and such, and then send them the *real* confirmation message using Lamson. Since the Confirmation API is Python you could do this directly in any Python web framework. You're basically moving the call to @CONFRIM.send@ from your @START@ handler into the web framework. # Then your web framework will have sent them a real confirmation email, not just a link, so when they reply, continue with the usual Lamson confirmation process described above. Doing it this way ends up being a good balance between too many clicks and replies, but too few to confirm that the end user can actually reply to email you send them. h2. The Pending Queue You should also notice in the above examples that the original message is stored in a "pending queue" and then given back to you later. This is handy for either finishing their original request without further intervention, or inspecting what they original wanted to do. In the original Librelist code I would take their first message, confirm them, and then pull it out of the pending queue to send it on. This turned out to not work because socially people "subscribe" with a garbage first message, but technically it worked great. bq. You may want to periodically go through this queue and purge any messages that aren't found in the ConfirmationsStorage. Probably with a simple Python script and a cronjob. h2. Conclusion The Lamson confirmation API encapsulates a pattern for confirming potential users. Feel free to suggest improvements to the API if you find further patterns that are needed. lamson-1.0pre11/doc/lamsonproject.org/output/docs/deferred_processing_to_queues.html0000644000076500000240000003115311242707507030552 0ustar zedshawstaff LamsonProject: Deferred Processing To Queues

Deferred Processing To Queues

As of the 0.9.2 release there is preliminary support for deferring handling of a mail message to a queue for another process to deal with in a separate handler. This support is rough at this time, but still useful and not too difficult to configure. As the feature gets more use it will improve and hopefully turn into a generic “defer to queue” system in Lamson.

What is meant by “defer to queue” is simply that you take messages your state function receives and you dump them into a maildir queue. You then have another separate process read from this queue and do the real work. Potentially you could have many processes deal with this work, and they could even be on multiple computers.

A More Concrete Example

Imagine that you have a blog posting system and you want to update a big “front page index” that shows recent posts by your users. However, you don’t want to generate this index on every blog post users make, since that could involve expensive computation and hold up other threads that need to deal with more urgent email.

The solution is to do the minimum quick processing you can in your POSTING state function, and then use the lamson.queue.Queue to queue up messages meant for “front page indexing”. Here’s how that code might go:

@route("(post_name)@osb\\.(host)")
def POSTING(message, post_name=None, host=None):
    # do the regular posting to blog thing
    name, address = parseaddr(message['from'])
    post.post(post_name, address, message)
    msg = view.respond('page_ready.msg', locals())
    relay.deliver(msg)

    # drop the message off into the 'posts' queue for later
    index_q = queue.Queue("run/posts")
    index_q.push(message)

    return POSTING

You can see that you just drop it into the queue with push(message) and it’s done. What you don’t see is how this then gets picked up by another process to actually do somehing with this email.

Configuring A config/queue.py

In Lamson you are given control over how your software boots, which gives you the ability to configure extra services how you need. By default the lamson gen command just outputs a basic config/boot.py and config/testing.py file so you can get working, and these will work for most development purposes.

In this tutorial you get to write a new boot configuration and tell Lamson how to use it. We’ll be copying the original boot file over first:

$ cp config/boot.py config/queue.py

Next you want to edit this file so that instead of running an SMTPReceiver it will use a QueueReceiver configured to pull out of the run/posts queue you are using in your POSTING handler.

...
# where to listen for incoming messages
settings.receiver = QueueReceiver(settings.queue_config['queue'],
                                  settings.queue_config['sleep'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
# NOTE: this is using a different handlers variable in settings
Router.load(settings.queue_handlers)
Router.RELOAD=True
...

I’ve removed the code above the … and below it since it’s the same in the two files. Notice that you have a QueueReceiver now, and that you are telling the Router that it will use settings.queue_handlers for the list of handlers to load and run.

You now add these two lines to your config/settings.py:

...
# this is for when you run the config.queue boot
queue_config = {'queue': 'run/posts', 'sleep': 10}

queue_handlers = ['app.handlers.index']

The queue_config variable is read by the config/queue.py file for the QueueReceiver and the queue_handlers is fed to the Router as described above.

Writing The Index Handler

You now have to write a new handler that is in app/handlers/index.py so that this config.queue boot setup will load it and run it whenever a message hits the run/queue. Here’s the code:

from lamson import queue
from lamson.routing import route, stateless
import logging

@route("(post_name)@osb\\.(host)")
@stateless
def START(message, post_name=None, host=None):
    logging.debug("Got message from %s", message['from'])

This simple demonstration will just log what messages it receives so you can make sure it is working.

There are two points to notice about this handler. First, it is marked stateless because it will run independent of the regular Lamson server, and you don’t want its parallel operations to interfere with your normal server’s state operations. Second, it uses a Router.defaults named post_name that you would add to your config.settings.router_defaults.

Once you have all this slightly complicated setup done you are ready to test it.

Also note that the examples in the source releases have code that does a deferred queue similar to this. Go look there for more code to steal.

Running Your Queue Receiver

Run your logger and lamson server like normal:

$ lamson log
$ lamson start

Next, go look in your logs and make sure it works by running your unit tests:

$ nosetests
................
----------------------------------------------------------------------
Ran 16 tests in 1.346s

OK

Your logs should look normal, but now you should see some files in the run/posts/new directory:

$ ls run/posts/new/
1244080328.M408474P3147Q4.mycomputer.local

That’s the results of your POSTING handler putting the messages it receives into your run/posts maildir queue.

Finally, you’ll want to run your queue receiver:

$ lamson start -boot config.queue -pid run/queue.pid

If you’re running the code given above then you should see this in the logs/lamson.log file:

...
DEBUG:root:Sleeping for 10 seconds...
DEBUG:root:Pulled message with key:
'1244080328.M408474P3147Q4.zed-shaws-macbook.local' off
DEBUG:root:Message received from Peer: 'run/posts', From:
'sender-1244080328.22@sender.com', to To
['test.blog.1244080328@osb.test.com'].
DEBUG:root:Got message from sender-1244080328.22@sender.com
DEBUG:root:Message to test.blog.1244080328@osb.test.com was handled by
app.handlers.index.START

Which means your queue receiver is running. You could in theory run as many of these as you wanted, as long as their handlers are stateless.

When you’re done you can stop the whole setup with the following command:

$ lamson stop -ALL run
Stopping processes with the following PID files:
['run/log.pid', 'run/queue.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 3092
Attempting to stop lamson at pid 3157
Attempting to stop lamson at pid 3096

Further Advanced Usage

This configuration is debatable whether it is very usable or not, but it works and will improve as the project continues. To give you some ideas of what you can do with it:

  1. Defer activity to other machines or processes.
  2. Receive messages from other mail systems that know maildir.
  3. Deliver messages to other maildir aware systems.
  4. Process messages from a web application, and possibly even generic work.

It might also be possible to actually make your state functions transition to the queue handler states by simply having the function return the module.FUNCTION that should be next. Take care with this though as it means your end user’s actions are effectively blocked for that event until the next run of the queue receiver.

Call For Suggestions

Feel free to offer suggestions in improving this setup (or even better code).


lamson-1.0pre11/doc/lamsonproject.org/output/docs/deferred_processing_to_queues.txt0000644000076500000240000001760111242707504030424 0ustar zedshawstaffTitle: Deferred Processing To Queues As of the 0.9.2 release there is preliminary support for deferring handling of a mail message to a queue for another process to deal with in a separate handler. This support is rough at this time, but still useful and not too difficult to configure. As the feature gets more use it will improve and hopefully turn into a generic "defer to queue" system in Lamson. What is meant by "defer to queue" is simply that you take messages your state function receives and you dump them into a maildir queue. You then have another separate process read from this queue and do the real work. Potentially you could have many processes deal with this work, and they could even be on multiple computers. h2. A More Concrete Example Imagine that you have a blog posting system and you want to update a big "front page index" that shows recent posts by your users. However, you don't want to generate this index on *every* blog post users make, since that could involve expensive computation and hold up other threads that need to deal with more urgent email. The solution is to do the minimum quick processing you can in your POSTING state function, and then use the "lamson.queue.Queue":http://lamsonproject.org/docs/api/lamson.queue.Queue-class.html to queue up messages meant for "front page indexing". Here's how that code might go:
@route("(post_name)@osb\\.(host)")
def POSTING(message, post_name=None, host=None):
    # do the regular posting to blog thing
    name, address = parseaddr(message['from'])
    post.post(post_name, address, message)
    msg = view.respond('page_ready.msg', locals())
    relay.deliver(msg)

    # drop the message off into the 'posts' queue for later
    index_q = queue.Queue("run/posts")
    index_q.push(message)

    return POSTING
You can see that you just drop it into the queue with @push(message)@ and it's done. What you don't see is how this then gets picked up by another process to actually do somehing with this email. h2. Configuring A config/queue.py In Lamson you are given control over how your software boots, which gives you the ability to configure extra services how you need. By default the @lamson gen@ command just outputs a basic @config/boot.py@ and @config/testing.py@ file so you can get working, and these will work for most development purposes. In this tutorial you get to write a new boot configuration and tell Lamson how to use it. We'll be copying the original boot file over first:
$ cp config/boot.py config/queue.py
Next you want to edit this file so that instead of running an "SMTPReceiver":http://lamsonproject.org/docs/api/lamson.server.SMTPReceiver-class.html it will use a "QueueReceiver":http://lamsonproject.org/docs/api/lamson.server.QueueReceiver-class.html configured to pull out of the @run/posts@ queue you are using in your @POSTING@ handler.
...
# where to listen for incoming messages
settings.receiver = QueueReceiver(settings.queue_config['queue'],
                                  settings.queue_config['sleep'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
# NOTE: this is using a different handlers variable in settings
Router.load(settings.queue_handlers)
Router.RELOAD=True
...
I've removed the code above the ... and below it since it's the same in the two files. Notice that you have a @QueueReceiver@ now, and that you are telling the "Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html that it will use @settings.queue_handlers@ for the list of handlers to load and run. You now add these two lines to your @config/settings.py@:
...
# this is for when you run the config.queue boot
queue_config = {'queue': 'run/posts', 'sleep': 10}

queue_handlers = ['app.handlers.index']
The @queue_config@ variable is read by the @config/queue.py@ file for the @QueueReceiver@ and the @queue_handlers@ is fed to the @Router@ as described above. h2. Writing The Index Handler You now have to write a new handler that is in @app/handlers/index.py@ so that this @config.queue@ boot setup will load it and run it whenever a message hits the @run/queue@. Here's the code:
from lamson import queue
from lamson.routing import route, stateless
import logging


@route("(post_name)@osb\\.(host)")
@stateless
def START(message, post_name=None, host=None):
    logging.debug("Got message from %s", message['from'])
This simple demonstration will just log what messages it receives so you can make sure it is working. There are two points to notice about this handler. First, it is marked @stateless@ because it will run independent of the regular Lamson server, and you don't want its parallel operations to interfere with your normal server's state operations. Second, it uses a @Router.defaults@ named @post_name@ that you would add to your @config.settings.router_defaults@. Once you have all this slightly complicated setup done you are ready to test it. bq. Also note that the examples in the "source releases":/releases/ have code that does a deferred queue similar to this. Go look there for more code to steal. h2. Running Your Queue Receiver Run your logger and lamson server like normal:
$ lamson log
$ lamson start
Next, go look in your logs and make sure it works by running your unit tests:
$ nosetests
................
----------------------------------------------------------------------
Ran 16 tests in 1.346s

OK
Your logs should look normal, but now you should see some files in the @run/posts/new@ directory:
$ ls run/posts/new/
1244080328.M408474P3147Q4.mycomputer.local
That's the results of your @POSTING@ handler putting the messages it receives into your @run/posts@ maildir queue. Finally, you'll want to run your queue receiver:
$ lamson start -boot config.queue -pid run/queue.pid
If you're running the code given above then you should see this in the @logs/lamson.log@ file:
...
DEBUG:root:Sleeping for 10 seconds...
DEBUG:root:Pulled message with key:
'1244080328.M408474P3147Q4.zed-shaws-macbook.local' off
DEBUG:root:Message received from Peer: 'run/posts', From:
'sender-1244080328.22@sender.com', to To
['test.blog.1244080328@osb.test.com'].
DEBUG:root:Got message from sender-1244080328.22@sender.com
DEBUG:root:Message to test.blog.1244080328@osb.test.com was handled by
app.handlers.index.START
Which means your queue receiver is running. You could *in theory* run as many of these as you wanted, as long as their handlers are stateless. When you're done you can stop the whole setup with the following command:
$ lamson stop -ALL run
Stopping processes with the following PID files:
['run/log.pid', 'run/queue.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 3092
Attempting to stop lamson at pid 3157
Attempting to stop lamson at pid 3096
h2. Further Advanced Usage This configuration is debatable whether it is very usable or not, but it works and will improve as the project continues. To give you some ideas of what you can do with it: # Defer activity to other machines or processes. # Receive messages from other mail systems that know maildir. # Deliver messages to other maildir aware systems. # Process messages from a web application, and possibly even generic work. It might also be possible to actually make your state functions transition to the queue handler states by simply having the function return the @module.FUNCTION@ that should be next. Take care with this though as it means your end user's actions are effectively blocked for that event until the next run of the queue receiver. h2. Call For Suggestions Feel free to offer suggestions in improving this setup (or even better code). lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_lamson.html0000644000076500000240000003033111242710571026000 0ustar zedshawstaff LamsonProject: Deploying Lamson Level 1

Deploying Lamson Level 1

These instructions will teach you how to setup a completely clean Python 2.6 installation, a virtualenv with lamson, and all the gear needed to run the oneshotblog.com software on your machine.

You can then read how to install oneshotblog for yourself.

Most of these instructions could be easily turned into an automated script, which may happen in the future. For now it is meant to teach you about the typically dirty details involved in setting up a system for the first time. It also tries to avoid various problems with different operating systems, so let me know how it works for you.

A Warning

Deploying server software is a notoriously nasty process, especially the first 10 or 20 times. Most operating systems do their best to enforce completely arbitrary restrictions on your file layouts and configurations, and every system has different arbitrary restrictions.

When you go through these instructions, make sure you stay awake and be ready to delve into why a particular step might not work on your system. There’s a good chance you missed something or that there’s something just slightly different about your system that makes the step not work.

For example, in the parts of this document where I setup oneshotblog.com I ran into a problem with SpamBayes dying because it couldn’t iterate a bsddb. Problem is this works just fine in the exact same setup on a CentOS machine and was only dying on a MacOSX machine that I later tested. For whatever reason, the exact same setup can’t run SpamBayes on OSX, even though it can run with the stock Python 2.5 in OSX.

To solve the problem I just had to show you how to disable SpamBayes in the oneshotblog.com code so you could test it. That’s just how deployment goes. You get on a machine and start setting things up and then 2/3 of the way through the configuration you find out that something doesn’t work.

Only choices are to work around the problem (like I did) or try to figure out why your machine is different and fix it.

Step 0: Setup A Workplace

You’ll want a directory to do this in so that you don’t screw up your machine. Here’s what I did:

$ mkdir deploy
$ cd deploy
$ export DEPLOY=$PWD

That last bit is so you can refer to this deployment directory with $DEPLOY (which I’ll be using in the instructions from now on).

Step 1: Get Python

Many operating systems have old versions of Python, and even though Lamson works with 2.6 or 2.5, you’ll probably want to get 2.6 for your deployment. If your OS has 2.6 available then go ahead and install it.

If it doesn’t have the right Python version, then here’s how you can install it from source and use it as your default Python. To do this, just punch in these commands:

$ mkdir source
$ cd source
$ wget http://www.python.org/ftp/python/2.6.2/Python-2.6.2.tgz
$ tar -xvf Python-2.6.2.tgz
$ cd Python-2.6.2
$ ./configure --prefix=$DEPLOY
$ make
$ make install

After this you will have a bunch of new directories in $DEPLOY:

$ ls $DEPLOY
bin     include lib     share   source

Finally, you just have to put this new bin directory into your $PATH:

$ export PATH=$DEPLOY/bin:$PATH

... then you just try it out to make sure that you have the right one:

Python-2.6.2 $ which python
$DEPLOY/deploy/bin/python

Python-2.6.2 $ python 
Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> ^D

That’s it, you’ll now be able to use this Python when you need to run your Lamson server, and setup a virtualenv (coming next) so that you’re walled off from the rest of the system.

Operating system fanatics will scoff at putting the python install in this directory, so if you want you can just install it to the default /usr/local on your system and deal with all the various clashes and conflicts you’ll have, especially if you are on an MacOSX machine.

Step 2: Install VirtualEnv

Now we need to create a “virtual environment” to install all your software. To do this we’ll need easy_install installed to your $DEPLOY directory:

$ cd $DEPLOY/source
$ wget http://peak.telecommunity.com/dist/ez_setup.py
$ python ez_setup.py 
$ which easy_install
$DEPLOY/bin/easy_install

As you can see, you now have a clean install of easy_install in your fresh $DEPLOY/bin directory for you to use. Now you need to install virtualenv:

$ easy_install --prefix $DEPLOY virtualenv
$ which virtualenv
$DEPLOY/bin/virtualenv

Make sure you use --prefix $DEPLOY above or you’ll install things into the default system setup even though easy_install is clearly and obviously running from a Python in a totally different location so easy_install should know that.

Step 3: Create Your VirtualEnv

With that you are ready to setup your virtual environment which will house your Lamson setup and fill it with the gear you need.

First up is getting your virtualenv created and activated:

$ cd $DEPLOY
$ virtualenv LAMSON
New python executable in LAMSON/bin/python
Installing setuptools............done.
$ cd LAMSON
$ . bin/activate

That’s pretty simple, and it tells you clearly that you are using the LAMSON virtualenv. It prepends that to your currently prompt, so your prompt may look different.

After that we can use easy_install to install our packages to this LAMSON virtual env. Keep in mind that these packages will be in $DEPLOY/LAMSON, so they won’t infect your regular $DEPLOY setup.

$ cd $DEPLOY/LAMSON
$ easy_install lamson

After that, you have lamson installed and ready to go, and you can install anything you want, but there is one catch:

You MUST be in the $DEPLOY/LAMSON directory or easy_install barfs complaining that the package is not there.

Step 4: Making Sure It Works

All of this setup is pointless if you can’t get back to it later, so exit your terminal completely and start a new one so you can do this:

$ cd projects/lamson/deploy/
$ export DEPLOY=$PWD
$ export PATH=$DEPLOY/bin:$PATH
$ cd $DEPLOY/LAMSON
$ . bin/activate
(LAMSON) $ which python
$DEPLOY/deploy/LAMSON/bin/python
(LAMSON) $ which easy_install
$DEPLOY/deploy/LAMSON/bin/easy_install
(LAMSON) $ which lamson
$DEPLOY/deploy/LAMSON/bin/lamson
(LAMSON) $ cd $DEPLOY
(LAMSON) $ lamson help

If you can do all that, then you know you’ve got the setup going, now you just need a little shell script to kick this all into gear automatically:

#!/bin/sh

export DEPLOY=$PWD
export PATH=$DEPLOY/bin:$PATH
cd $1
source bin/activate
cd $DEPLOY

To use this script, you just do this:

$ cd projects/lamson/deploy
$ . activate LAMSON

With that you have a fully ready to go setup that’s not using your normal system’s Python at all, has Python 2.6 installed, a fully virtualenv, and the start of your lamson setup.

Conclusion

Your next step is to try and setup oneshotblog using the instructions I’ve written to follow these instructions.

This document is very fresh, so send me feedback on your experience with running through it. Make sure you tell me what system you are on and that you ran each command exactly when you do.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_lamson.txt0000644000076500000240000001674611242710567025676 0ustar zedshawstaffTitle: Deploying Lamson Level 1 These instructions will teach you how to setup a completely clean Python 2.6 installation, a virtualenv with lamson, and all the gear needed to run the "oneshotblog.com":http://oneshotblog.com/ software on your machine. You can then "read how to install oneshotblog":/docs/deploying_oneshotblog.html for yourself. Most of these instructions could be easily turned into an automated script, which may happen in the future. For now it is meant to teach you about the typically dirty details involved in setting up a system for the first time. It also tries to avoid various problems with different operating systems, so let me know how it works for you. h2. A Warning Deploying server software is a notoriously nasty process, especially the first 10 or 20 times. Most operating systems do their best to enforce completely arbitrary restrictions on your file layouts and configurations, and every system has different arbitrary restrictions. When you go through these instructions, make sure you stay awake and be ready to delve into why a particular step might not work on your system. There's a good chance you missed something or that there's something just slightly different about your system that makes the step not work. For example, in the parts of this document where I setup "oneshotblog.com":http://oneshotblog.com/ I ran into a problem with "SpamBayes":http://spambayes.sourceforge.net/ dying because it couldn't iterate a bsddb. Problem is this works just fine in the exact same setup on a CentOS machine and was only dying on a MacOSX machine that I later tested. For whatever reason, the exact same setup can't run SpamBayes on OSX, even though it can run with the stock Python 2.5 in OSX. To solve the problem I just had to show you how to disable SpamBayes in the oneshotblog.com code so you could test it. That's just how deployment goes. You get on a machine and start setting things up and then 2/3 of the way through the configuration you find out that something doesn't work. Only choices are to work around the problem (like I did) or try to figure out why your machine is different and fix it. h2. Step 0: Setup A Workplace You'll want a directory to do this in so that you don't screw up your machine. Here's what I did:
$ mkdir deploy
$ cd deploy
$ export DEPLOY=$PWD
That last bit is so you can refer to this deployment directory with $DEPLOY (which I'll be using in the instructions from now on). h2. Step 1: Get Python Many operating systems have old versions of Python, and even though Lamson works with 2.6 or 2.5, you'll probably want to get 2.6 for your deployment. If your OS has 2.6 available then go ahead and install it. If it doesn't have the right Python version, then here's how you can install it from source and use it as your default Python. To do this, just punch in these commands:
$ mkdir source
$ cd source
$ wget http://www.python.org/ftp/python/2.6.2/Python-2.6.2.tgz
$ tar -xvf Python-2.6.2.tgz
$ cd Python-2.6.2
$ ./configure --prefix=$DEPLOY
$ make
$ make install
After this you will have a bunch of new directories in $DEPLOY:
$ ls $DEPLOY
bin     include lib     share   source
Finally, you just have to put this new bin directory into your $PATH:
$ export PATH=$DEPLOY/bin:$PATH
... then you just try it out to make sure that you have the right one:
Python-2.6.2 $ which python
$DEPLOY/deploy/bin/python

Python-2.6.2 $ python 
Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> ^D
That's it, you'll now be able to use this Python when you need to run your Lamson server, and setup a virtualenv (coming next) so that you're walled off from the rest of the system. bq. Operating system fanatics will scoff at putting the python install in this directory, so if you want you can just install it to the default /usr/local on your system and deal with all the various clashes and conflicts you'll have, especially if you are on an MacOSX machine. h2. Step 2: Install VirtualEnv Now we need to create a "virtual environment" to install all your software. To do this we'll need easy_install installed to your $DEPLOY directory:
$ cd $DEPLOY/source
$ wget http://peak.telecommunity.com/dist/ez_setup.py
$ python ez_setup.py 
$ which easy_install
$DEPLOY/bin/easy_install
As you can see, you now have a clean install of easy_install in your fresh $DEPLOY/bin directory for you to use. Now you need to install @virtualenv@:
$ easy_install --prefix $DEPLOY virtualenv
$ which virtualenv
$DEPLOY/bin/virtualenv
bq. Make sure you use @--prefix $DEPLOY@ above or you'll install things into the default system setup even though easy_install is clearly and obviously running from a Python in a totally different location so easy_install should know that. h2. Step 3: Create Your VirtualEnv With that you are ready to setup your virtual environment which will house your Lamson setup and fill it with the gear you need. First up is getting your virtualenv created and activated:
$ cd $DEPLOY
$ virtualenv LAMSON
New python executable in LAMSON/bin/python
Installing setuptools............done.
$ cd LAMSON
$ . bin/activate
That's pretty simple, and it tells you clearly that you are using the LAMSON virtualenv. It prepends that to your currently prompt, so your prompt may look different. After that we can use easy_install to install our packages to this LAMSON virtual env. Keep in mind that these packages will be in $DEPLOY/LAMSON, so they won't infect your regular $DEPLOY setup.
$ cd $DEPLOY/LAMSON
$ easy_install lamson
After that, you have lamson installed and ready to go, and you can install anything you want, but there is one catch: bq. You *MUST* be in the $DEPLOY/LAMSON directory or easy_install barfs complaining that the package is not there. h2. Step 4: Making Sure It Works All of this setup is pointless if you can't get back to it later, so exit your terminal completely and start a new one so you can do this:
$ cd projects/lamson/deploy/
$ export DEPLOY=$PWD
$ export PATH=$DEPLOY/bin:$PATH
$ cd $DEPLOY/LAMSON
$ . bin/activate
(LAMSON) $ which python
$DEPLOY/deploy/LAMSON/bin/python
(LAMSON) $ which easy_install
$DEPLOY/deploy/LAMSON/bin/easy_install
(LAMSON) $ which lamson
$DEPLOY/deploy/LAMSON/bin/lamson
(LAMSON) $ cd $DEPLOY
(LAMSON) $ lamson help
If you can do all that, then you know you've got the setup going, now you just need a little shell script to kick this all into gear automatically:
#!/bin/sh

export DEPLOY=$PWD
export PATH=$DEPLOY/bin:$PATH
cd $1
source bin/activate
cd $DEPLOY
To use this script, you just do this:
$ cd projects/lamson/deploy
$ . activate LAMSON
With that you have a fully ready to go setup that's not using your normal system's Python at all, has Python 2.6 installed, a fully virtualenv, and the start of your lamson setup. h2. Conclusion Your next step is to try and setup "oneshotblog":http://oneshotblog.com/ using the "instructions I've written":/docs/deploying_oneshotblog.html to follow these instructions. This document is very fresh, so send me feedback on your experience with running through it. Make sure you tell me what system you are on and that you ran each command exactly when you do. lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_lamson_level_2.html0000644000076500000240000000767511243030345027422 0ustar zedshawstaff LamsonProject: Deploying Lamson Level 2

Deploying Lamson Level 2

Coming soon…


lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_lamson_level_2.txt0000644000076500000240000000006011243027730027256 0ustar zedshawstaffTitle: Deploying Lamson Level 2 Coming soon... lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_librelist.html0000644000076500000240000000765411243030345026507 0ustar zedshawstaff LamsonProject: Deploying Librelist

Deploying Librelist

Coming soon…

lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_librelist.txt0000644000076500000240000000005411243027650026352 0ustar zedshawstaffTitle: Deploying Librelist Coming soon... lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_oneshotblog.html0000644000076500000240000003447611242710515027046 0ustar zedshawstaff LamsonProject: Deploying OneShotBlog

Deploying OneShotBlog

These instructions follow from Deploying Lamson Level 1 and you should follow those first before attempting these. If you run into problems with these instructions, then email the lamson@librelist.com mailing list for help.

Step 5: Setting Up The OneShotBlog

Let’s see if we can setup the OneShotBlog example from the Lamson source the way it is on the oneshotblog.com site. We’ll need a few more modules installed with easy_install:

$ cd $DEPLOY/LAMSON
$ easy_install markdown
$ easy_install mock
$ easy_install spambayes

Let’s grab the 0.9.3 source from PyPI so we can get at the OSB example source:

$ cd $DEPLOY/source
$ wget http://pypi.python.org/packages/source/l/lamson/lamson-0.9.3.tar.gz
$ tar -xzf lamson-0.9.3.tar.gz
$ cd lamson-0.9.3/examples/osb

Now we hit a slight snag. OSB is using SpamBayes to do spam filtering, but you probably will have a broken setup and would need to configure a ton of stuff to get it working. For now we’re, just going to cheat, since it looks like SpamBayes has problems with trying to iterate the keys in a bsddb under some Python builds. To avoid the problem, we’re just going to edit app/handlers/comment.py to remove the line with spam_filter:

@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
# DELETE THIS LINE IN app/handlers/comments.py
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, user_id=None, post_name=None, host=None, domain=None):
    comment.attach_headers(message, user_id, post_name, domain) 
    confirmation.send(relay, "comment", message, "mail/comment_confirm.msg", locals())
    return CONFIRMING

The spam filtering does work, but SpamBayes is difficult to get working in such a small test run.

You are now sitting in the OSB example code, so you can fire up the logger server and run the unit tests to make sure everything is working:

$ mkdir logs
$ mkdir run
$ mkdir app/data/posts
$ lamson log
$ nosetests

You should get two errors you can ignore for now:

..............
======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_unconfirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_confirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured stdout << ---------------------
run/posts count after dever 1
run/posts count after dever 2

--------------------- >> end captured stdout << ----------------------
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 25 tests in 1.363s

Those are just fine since you don’t have PyEnchant installed and aren’t using the spam filtering.

Step 6: Run OneShotBlog Example

Now you’re running the logger server and have your unit tests going, and hopefully you can fix anything that you run into by now. All you need now is to run the whole setup and try it out:

$ lamson start
$ lamson start -pid run/queue.pid -boot config.queue
$ lamson start -pid run/forward.pid -boot config.forward

With all this gear running you should be able to look in the logs/lamson.log and logs/logger.log to see what’s going on. You’ll see the following activity:

  • Forwarding receiver taking mail that couldn’t be delivered and forwarding it to the logger server.
  • The queue receiver pulling messages off run/posts and either delivering them as comments or updating the index.
  • The rest of lamson processing mail and doing its job of feeding these two or just sending emails.

If you want to see the configuration for these two other servers look in config/queue.py and config/forward.py or better yet, diff them against config/boot.py to see what’s really different.

You should also check to see that they are really running:

$ ps ax | grep lamson
29438   ??  S 0:05.78 python lamson log
29605   ??  S 0:00.63 python lamson start
29612   ??  S 0:00.19 python lamson start -pid run/queue.pid -boot config.queue
29617   ??  S 0:00.34 python lamson start -pid run/forward.pid -boot config.forward

Step 7: Playing With OneShotBlog

Now we get to play with it. Lamson comes with a web server that you can run to do simple testing so start up a second window/terminal and do this:

$ cd projects/lamson/deploy/
$ . activate LAMSON
(LAMSON) $ cd source/lamson-0.9.3/examples/osb/
(LAMSON) $ lamson web -basedir app/data
Starting server on 127.0.0.1:8888 out of directory 'app/data'

Now hit http://localhost:8888/ with your browser and see the junk left over from your test runs. Most of those posts won’t actually exist, so let’s make a fake one for now.

You can forget about this web server window for now, and go back to your LAMSON window to do this with mutt:

  1. mutt -F muttrc to get it going with a fake setup.
  2. Send an email to first.blog@oneshotblog.com (m is the key).
  3. You’ll get a confirmation back, reply to it.
  4. You’ll get a welcome message, but this message isn’t in the index yet. You can go look at it directly though.
  5. Send another email, this time to my.new.post@oneshotblog.com.
  6. No confirmation this time, just a message saying it was completed.
  7. Now go look at the index (might take a few seconds, up to 10).
  8. Click on the post title and go look at it.
  9. Right click on the [send comment] link and copy the email address.
  10. Go back to mutt and send an email to that address, this will post a comment to that post.
  11. Reply to the comment confirmation email, this should be the only one you get.
  12. In about 10 seconds you’ll see your comment show up.

With that you have fully tested out the OneShotBlog example. All that remains would be a full deployment in a for-real situation, which is what we’ll do next.

Step 8: Running On Port 25 For Real

The only problem with testing out the OneShotBlog with your own email client is that you need to trick your computer into thinking your localhost address is also oneshotblog.com. To do that, open your /etc/hosts file and make whatever changes you need to have localhost be oneshotblog.com also.

You’ll know you’ve got it right when you can point your browser at oneshotblog.com and see your lamson web window display log messages showing you click around.

Remember to undo this or you might be annoyed later.

Next, you’ll need to stop lamson and restart it to use port 25. This will be a problem if you have another server running on port 25, so make sure you turn that server off for now.

I hope you aren’t doing this on a live site where that’s a problem.

$ lamson stop
$ vim config/settings.py

At this point you’ll want to change the receiver_config to look like this in config/settings.py:

receiver_config = {'host': 'localhost', 'port': 25}

Then you’ll want to restart lamson so that it drops privilege to your user after grabbing that port. Easiest way to find out what your user id (uid) and group id (gid) are is to use Python:

Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getuid()
500
>>> os.getgid()
500
>>> ^D

This shows that I’m uid 500 and gid 500, so now I can start my server:

$ sudo lamson start -uid 500 -gid 500
$ sudo chown -R zedshaw ./

That last command just makes sure that lamson didn’t accidentally change the permission of a stray file or queue to root.

With that you should be running your lamson server as your user rather than as root, but still bound to port 25. Here’s how to check:

$ ps aux | grep lamson | grep root

If you don’t see anything printed out, then you’re safe. If you see something running as root, then you’ve got some work to do.

Conclusion

I’ll leave it to you to actually get your mail client to talk to this OneShotBlog and working. If you have problems, look at all the files in logs/ and also use the lamson queue command to inspect the different queues in the run/ directory.

At this point, you should know enough about setting up a lamson server and configuring a real application (warts and all). You should also have learned how to get a clean Python installation that you can use no matter what your native OS does to Python, even if it’s retarded and renames python to Python for no apparent reason.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/deploying_oneshotblog.txt0000644000076500000240000002261411242710431026705 0ustar zedshawstaffTitle: Deploying OneShotBlog These instructions follow from "Deploying Lamson Level 1":/docs/deploying_lamson.html and you should follow those first before attempting these. If you run into problems with these instructions, then "email the lamson@librelist.com":mailto:lamson@librelist.com mailing list for help. h2. Step 5: Setting Up The OneShotBlog Let's see if we can setup the OneShotBlog example from the Lamson source the way it is on the "oneshotblog.com":http://oneshotblog.com site. We'll need a few more modules installed with easy_install:
$ cd $DEPLOY/LAMSON
$ easy_install markdown
$ easy_install mock
$ easy_install spambayes
Let's grab the 0.9.3 source from "PyPI":http://pypi.python.org/pypi/lamson/0.9.3 so we can get at the OSB example source:
$ cd $DEPLOY/source
$ wget http://pypi.python.org/packages/source/l/lamson/lamson-0.9.3.tar.gz
$ tar -xzf lamson-0.9.3.tar.gz
$ cd lamson-0.9.3/examples/osb
Now we hit a slight snag. OSB is using "SpamBayes":http://spambayes.sourceforge.net/ to do spam filtering, but you probably will have a broken setup and would need to configure a ton of stuff to get it working. For now we're, just going to cheat, since it looks like SpamBayes has problems with trying to iterate the keys in a bsddb under *some* Python builds. To avoid the problem, we're just going to edit @app/handlers/comment.py@ to remove the line with @spam_filter@:
@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
# DELETE THIS LINE IN app/handlers/comments.py
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, user_id=None, post_name=None, host=None, domain=None):
    comment.attach_headers(message, user_id, post_name, domain) 
    confirmation.send(relay, "comment", message, "mail/comment_confirm.msg", locals())
    return CONFIRMING
The spam filtering does work, but SpamBayes is difficult to get working in such a small test run. You are now sitting in the OSB example code, so you can fire up the logger server and run the unit tests to make sure everything is working:
$ mkdir logs
$ mkdir run
$ mkdir app/data/posts
$ lamson log
$ nosetests
You should get two errors you can ignore for now:
..............
======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_unconfirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: handlers.comments_tests.test_spam_sent_by_confirmed_user
----------------------------------------------------------------------
Traceback (most recent call last):
    ...
-------------------- >> begin captured stdout << ---------------------
run/posts count after dever 1
run/posts count after dever 2

--------------------- >> end captured stdout << ----------------------
-------------------- >> begin captured logging << --------------------
root: WARNING: Attempt to post to user 'spamtester@somehost.com' but user doesn't exist.
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 25 tests in 1.363s
Those are just fine since you don't have PyEnchant installed and aren't using the spam filtering. h2. Step 6: Run OneShotBlog Example Now you're running the logger server and have your unit tests going, and hopefully you can fix anything that you run into by now. All you need now is to run the whole setup and try it out:
$ lamson start
$ lamson start -pid run/queue.pid -boot config.queue
$ lamson start -pid run/forward.pid -boot config.forward
With all this gear running you should be able to look in the @logs/lamson.log@ and @logs/logger.log@ to see what's going on. You'll see the following activity: * Forwarding receiver taking mail that couldn't be delivered and forwarding it to the logger server. * The queue receiver pulling messages off run/posts and either delivering them as comments or updating the index. * The rest of lamson processing mail and doing its job of feeding these two or just sending emails. bq. If you want to see the configuration for these two other servers look in @config/queue.py@ and @config/forward.py@ or better yet, diff them against @config/boot.py@ to see what's really different. You should also check to see that they are really running:
$ ps ax | grep lamson
29438   ??  S 0:05.78 python lamson log
29605   ??  S 0:00.63 python lamson start
29612   ??  S 0:00.19 python lamson start -pid run/queue.pid -boot config.queue
29617   ??  S 0:00.34 python lamson start -pid run/forward.pid -boot config.forward
h2. Step 7: Playing With OneShotBlog Now we get to play with it. Lamson comes with a web server that you can run to do simple testing so start up a second window/terminal and do this:
$ cd projects/lamson/deploy/
$ . activate LAMSON
(LAMSON) $ cd source/lamson-0.9.3/examples/osb/
(LAMSON) $ lamson web -basedir app/data
Starting server on 127.0.0.1:8888 out of directory 'app/data'
Now hit "http://localhost:8888/":http://localhost:8888/ with your browser and see the junk left over from your test runs. Most of those posts won't actually exist, so let's make a fake one for now. You can forget about this web server window for now, and go back to your LAMSON window to do this with mutt: # mutt -F muttrc to get it going with a fake setup. # Send an email to first.blog@oneshotblog.com (m is the key). # You'll get a confirmation back, reply to it. # You'll get a welcome message, but this message isn't in the index yet. You can go look at it directly though. # Send *another* email, this time to my.new.post@oneshotblog.com. # No confirmation this time, just a message saying it was completed. # *Now* go look at the index (might take a few seconds, up to 10). # Click on the post title and go look at it. # Right click on the [send comment] link and copy the email address. # Go back to mutt and send an email to that address, this will post a comment to that post. # Reply to the comment confirmation email, this should be the only one you get. # In about 10 seconds you'll see your comment show up. With that you have fully tested out the OneShotBlog example. All that remains would be a full deployment in a for-real situation, which is what we'll do next. h2. Step 8: Running On Port 25 For Real The only problem with testing out the OneShotBlog with your own email client is that you need to trick your computer into thinking your localhost address is also oneshotblog.com. To do that, open your /etc/hosts file and make whatever changes you need to have localhost be oneshotblog.com also. You'll know you've got it right when you can point your browser at "oneshotblog.com":http://oneshotblog.com/ and see your @lamson web@ window display log messages showing you click around. bq. Remember to undo this or you might be annoyed later. Next, you'll need to stop lamson and restart it to use port 25. This will be a problem if you have another server running on port 25, so make sure you turn that server off for now. bq. I hope you aren't doing this on a live site where that's a problem.
$ lamson stop
$ vim config/settings.py
At this point you'll want to change the receiver_config to look like this in @config/settings.py@:
receiver_config = {'host': 'localhost', 'port': 25}
Then you'll want to restart lamson so that it drops privilege to your user after grabbing that port. Easiest way to find out what your user id (uid) and group id (gid) are is to use Python:
Python 2.6.2 (r262:71600, Jun  8 2009, 00:44:56) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getuid()
500
>>> os.getgid()
500
>>> ^D
This shows that I'm uid 500 and gid 500, so now I can start my server:
$ sudo lamson start -uid 500 -gid 500
$ sudo chown -R zedshaw ./
That last command just makes sure that lamson didn't accidentally change the permission of a stray file or queue to @root@. With that you should be running your lamson server as your user rather than as root, but still bound to port 25. Here's how to check:
$ ps aux | grep lamson | grep root
If you don't see anything printed out, then you're safe. If you see something running as root, then you've got some work to do. h2. Conclusion I'll leave it to you to actually get your mail client to talk to this OneShotBlog and working. If you have problems, look at all the files in @logs/@ and also use the @lamson queue@ command to inspect the different queues in the @run/@ directory. At this point, you should know enough about setting up a lamson server and configuring a real application (warts and all). You should also have learned how to get a clean Python installation that you can use no matter what your native OS does to Python, even if it's retarded and renames python to Python for no apparent reason. lamson-1.0pre11/doc/lamsonproject.org/output/docs/faq.html0000644000076500000240000004751411242712467023225 0ustar zedshawstaff LamsonProject: Frequently Asked Questions

Frequently Asked Questions

If a question is missing you can email the lamson@librelist.com mailing list about it and I’ll answer.

What is Lamson?

Lamson is a pure Python SMTP server designed to create robust and complex mail applications in the style of modern web frameworks such as Django. Unlike traditional SMTP servers like Postfix or Sendmail, Lamson has all the features of a web application stack (ORM, templates, routing, handlers, state machines, Python) without needing to configure alias files, run newaliases, or juggle tons of tiny fragile processes. Lamson also plays well with other web frameworks and Python libraries.

Where does the name “Lamson” come from?

“Lamson Tubes” is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today.

What kind of applications do you envision being built on top of Lamson?

Why, spam of course (like I could stop that). As well as spam fighters, greylisters, “campaign management” applications, mail firewalls, mime strippers (for those Exchange file shares), help desk support applications, games, mailing lists ('cause everyone loves writing those), and even SMTP portions of just about any web site.

A few examples of applications that are actually written using Lamson (with source included in the source releases

With many more to come.

How do I install lamson on a Debian or CentOS server?

Debian and CentOS are notorious for being dinosaurs. Both distributions of Linux suffer from the false rationale that older software is more stable and secure. The reality is that the stability or security of a piece of software is not a function of its age, and in many cases the newer versions of software will typically fix many stability and performance problems. Despite this fact, these two variants of Linux are notorious for back-porting patches from later versions to older versions rather than just using the newer version.

In order to help people who need to run a modern piece of software on their antiquated operating systems, I’ve written an extensive document on how to Deploy Lamson that tells you how to do it in a way that should work reliably on most Unix platforms, even if they have an older version of Python.

Is there a mailing list?

Yes, and it’s written in Lamson. You can send an email to lamson@librelist.com and you’ll be able to join. The code for this mailing list system is also included in the source releases so you can learn from it.

Is there an IRC channel?

Yes, and it has relatively low traffic. You can join #librelist on irc.freenode.org.

How do I report a BUG/Feature/Question in Lamson?

Currently the best place to report a Lamson bug is to the lamson@librelist.com mailing list. If you don’t want to subscribe to the list to just report a bug, then you can contact me directly to report it.

How do I work on Lamson?

I currently do all the work on Lamson myself. I don’t want to discourage contributions, but I’ve found that when a project is small and just getting started it’s best to keep it under the control of one person.

If you find bugs, then please report them to the lamson@librelist.com mailing list to to me directly and I’ll fix them up.

How do I try out (install) Lamson?

Best way to do it is to have easy_install and simply do:

$ sudo easy_install lamson

Then you’ll get it installed and can play with it. Refer to the getting started documentation for more information.

How can I get the code to Lamson?

Lamson lives on Launchpad at https://launchpad.net/lamson where you can get the code via:

bzr branch lp:lamson

Bazaar may ask you to login, but it should still give you the source.

Refer to the download instructions for more information.

What features are planned for the 1.0 release?

I try to make “1.0” what most other people would consider 80% complete. This lets people get it and work with it, and then I can refine it for the 2.0 release with 100% of features people actually use.

With this in mind, I’m planning on the 1.0 release to be based on the sample applications and my own applications and include the smallest set of features.

The most important part of the 1.0 release will be good documentation and bug free high quality code.

What are Lamson’s current features?

Lamson is currently running a handful of sites and is almost ready for a 1.0 release. It has these high level features:

  • Very good bounce detection and analysis.
  • Spam blocking.
  • Get an application going quickly with the “lamson gen” command.
  • Full set of self-documented commands for managing and developing a lamson application.
  • Lots of documentation with more to come.
  • Sample applications for you to base your work on.
  • Handle mail for arbitrary hosts and addresses using simple regex routing.
  • Process requests including full access to Python’s complete MIME email libraries.
  • Absolutely awesome full conversion to Unicode including complete cleaning of incoming mail.
  • Routing requests in a standard web application framework style based on addresses.
  • Processing mail messages (requests) in either simple generic handlers, or in more complex and robust state machines.
  • Use any Python database storage you want.
  • Use of Jinja2 or Mako templates, with Jinja2 as the default.
  • Craft and send plain text or HTML email including attachments, with a great HTML mail generation API that even “knits” in CleverCSS.
  • Unit test helpers for having a conversation, checking spelling, etc.
  • Defer processing to Maildir queues for offline handling of bigger tasks.
  • Extensive logging and development tools for debugging your email applications.
  • Mutt configurations to fake out mutt so that it talks to your server.
  • 100% code coverage in the unit tests.

Isn’t Postfix/Sendmail/Exim Faster?

That is a tough question to answer actually. If all you need to do is receive and deliver email then a well established traditional email server like Postfix or Exim (please don’t use Sendmail) is the way to go. Hands down these servers are the fastest and the best at this job.

However, if you need to actually do something smart with your email, like manage many mailing lists or handle support requests, then these servers are definitely slower.

The reason is they require that you configure them to take messages they’ve already received and hand them to a separate process like a Perl, Python, or Ruby script. This separate process then has to parse the message again, do its job without stepping on any other processes that might be running (that means locks), and then send response messages back to the server for even more SMTP parsing.

With the triple and sometimes quadruple MIME parsing, the heavy weight processes, the difficult to manage locking, and the additional configuration headaches, there’s no way traditional mail servers beat Lamson in speed.

Lamson only processes a message once, maybe twice if you defer to a queue. Once the message is parsed you get full access to Python immediately, without spawning a separate process. Even if you defer to a queue, the Lamson dequeue server stays resident and processes the queue without forking. You can even run many dequeue servers on mulitple machines processing a shared Maildir if you need the extra processing.

In the end, threads and function calls beats processes and pipes.

Why not use Sendmail’s Milter?

Sendmail has a protocol named “Milter” that lets you write a mail processing server that acts as a sort of “slave” to the sendmail process. This protocol is supported by at least Postfix as well, maybe other servers.

Feel free to go try Milter. When you’re done trying to figure out the protocol from the dense C code, configure the m4 macros, find a decent milter protocol library that doesn’t involve installing sendmail, and debugging the final setup, then you can come back and have it easy with Lamson.

Why does Lamson send messages to a relay host?

Lamson doesn’t have to deliver to a relay host, but it is a smarter more practical use of the technology.

Lamson is written in Python and does actually run slower than the established mail servers. In addition, Lamson is hopefully doing something more than just routing email around to people. It is probably processing messages, crafting replies, querying databases, hitting REST interfaces, and all the other things you’d want to do with a modern application. This takes time and resources and are probably more valuable operations than just simple delivery.

For this reason, you want to use a dumb workhorse like Postfix to do your actual delivery, and reserve the smart processing that has value for Lamson.

What about security?! Shouldn’t Lamson be 20 processes?

Have you ever asked why other mail servers are a bundle of a billion processes? Why have one server receiving mail, another routing it, and another handing it to users?

The answer is back in the 1970’s most mail was delivered to Unix users in their home directories or similar files that required special access rights to modify. Also in the 1970’s special ports like 25 for SMTP required root access, which in the tiny Internet of the time meant that the server could be “trusted”. These two realities of the time meant that to receive and deliver mail at least some part of the system had to run as root. To keep things safe, modern mail systems reduce the amount of time spent as the root user by separating their functionality into different processes.

However, if you never have to deliver to a user, and all you ever do is process mail and talk to other servers like RDBMS, then why do you need all this privilege separation? Sites run just fine with systems running as one or two processes without the complexity of some illogical privilege separation getting in their way.

To put this into perspective, imagine that you were writing a Django application and you were required to have a separate process for the HTTP layers, the view layer, the model layer, the HTTP responses, and the RDBMS access layers? Each one required a different user, a different configuration file, and you needed another process just to keep them all sane. All of this just so that if someone hacks into your HTTP server as root they supposedly can’t cause any damage.

Yet, they are on your server as root after all.

In practice, you can run Lamson as a separate root process, and then use another “dequeue server” to do the real processing, if you feel you need that security.

But, consider delaying that decision until you absolutely need it, because the security benefits aren’t worth the development and deployment hassles.

How come nobody thought of this before?

I don’t know why, since it did seem kind of simple. There’s at least one other project written in Perl called qpsmtpd that does something similar, and there may be more. If you know others feel free to contact me and let me know.

Isn’t [Insert Random Java Mail Server] actually the first mail “framework”?

I get this quite frequently when I make the claim that Lamson is the first email framework, and it may be true that there was a framework out there before Lamson. The internet is a big place, so anything is possible. However, I looked really hard and I couldn’t find a single modern mail framework. All that existed were servers I could use to build a framework.

You see, around 2003 or 2004 the concept of “framework” changed. Before then all you needed was a server with an extension API named so that it rhymed with “Servlet”. As long as your server provided a way to drop a class into the processing queue and let a programmer handle the request you could call that a framework.

The usual end result for these servers is that you could use them to build a framework if you wanted, but what you’d get is affectionately called a “frankenstack”. You’d grab an ORM from here, a template system from there, maybe a workflow engine, write a Maven or Ant script to manage it, and wire it all together with some lame secret sauce code you think gives you a competitive edge.

Then along came the modern frameworks like Django and Rails that included everything you needed in a bundle that you could use right away. They had ORM, templating, routing, higher level request processing, email support, REST support, and anything else you might need for the 80% of your application you don’t care about.

Some people prefer less of these defaults, some people more, but nearly everyone who has to get a project done prefers more than just an extension API so they can build their own framework.

Today if you try to claim Apache James is a framework you’d be wrong. I could build a framework with it, but I could just as easily build that same framework with Python, Ruby, sendmail and even postfix. James and friends are just servers, not frameworks. In fact, my experience with James and similar Java mail servers is they are much harder to use than aliases+pipes in Postfix.

I now advocate that if your framework doesn’t at least support data, views, and high level logic as first class entities then it’s just a server. You don’t have to use ORM, any particular templating, or Finite State Machines like Lamson does. You don’t even have to settle on only one way to do data, views, and logic.

You must at least support data, views, and logic out of the box so your user doesn’t have to go shopping at “APIs-R-Us” just to use your gear.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/faq.txt0000644000076500000240000003365211242712465023074 0ustar zedshawstaffTitle: Frequently Asked Questions If a question is missing you can "email the lamson@librelist.com mailing list":mailto:lamson@librelist.com about it and I'll answer. h2. What is Lamson? Lamson is a pure Python SMTP server designed to create robust and complex mail applications in the style of modern web frameworks such as Django. Unlike traditional SMTP servers like Postfix or Sendmail, Lamson has all the features of a web application stack (ORM, templates, routing, handlers, state machines, Python) without needing to configure alias files, run newaliases, or juggle tons of tiny fragile processes. Lamson also plays well with other web frameworks and Python libraries. h2. Where does the name "Lamson" come from? "Lamson Tubes" is a colloquial name for Pneumatic Tubes which were used last century to deliver mail, packages, and hazardous material to the corporate world. They are still in use today. h2. What kind of applications do you envision being built on top of Lamson? Why, spam of course (like I could stop that). As well as spam fighters, greylisters, "campaign management" applications, mail firewalls, mime strippers (for those Exchange file shares), help desk support applications, games, mailing lists ('cause everyone loves writing those), and even SMTP portions of just about any web site. A few examples of applications that are actually written using Lamson (with source included in the "source releases":/releases/ * "OneShotBlog":http://oneshotblog.com/ * "Librelist":http://librelist.com/ * "MyInboxIsNotA.TV":http://myinboxisnota.tv/ With many more to come. h2. How do I install lamson on a Debian or CentOS server? Debian and CentOS are notorious for being dinosaurs. Both distributions of Linux suffer from the false rationale that older software is more stable and secure. The reality is that the stability or security of a piece of software is not a function of its age, and in many cases the newer versions of software will typically fix many stability and performance problems. Despite this fact, these two variants of Linux are notorious for back-porting patches from later versions to older versions rather than just using the newer version. In order to help people who need to run a modern piece of software on their antiquated operating systems, I've written an extensive document on "how to Deploy Lamson":/docs/deploying_lamson.html that tells you how to do it in a way that should work reliably on most Unix platforms, even if they have an older version of Python. h2. Is there a mailing list? Yes, and it's written in Lamson. You can send an email to "lamson@librelist.com":mailto:lamson@librelist.com and you'll be able to join. The code for this mailing list system is also included in the "source releases":/releases/ so you can learn from it. h2. Is there an IRC channel? Yes, and it has relatively low traffic. You can join #librelist on irc.freenode.org. h2. How do I report a BUG/Feature/Question in Lamson? Currently the best place to report a Lamson bug is to the "lamson@librelist.com":mailto:lamson@librelist.com mailing list. If you don't want to subscribe to the list to just report a bug, then you can "contact me":/contact.html directly to report it. h2. How do I work on Lamson? I currently do all the work on Lamson myself. I don't want to discourage contributions, but I've found that when a project is small and just getting started it's best to keep it under the control of one person. If you find bugs, then please report them to the "lamson@librelist.com":mailto:lamson@librelist.com mailing list to "to me directly":/contact.html and I'll fix them up. h2. How do I try out (install) Lamson? Best way to do it is to have "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall and simply do:
$ sudo easy_install lamson
Then you'll get it installed and can play with it. Refer to the "getting started":/docs/getting_started.html documentation for more information. h2. How can I get the code to Lamson? Lamson lives on Launchpad at "https://launchpad.net/lamson":https://launchpad.net/lamson where you can get the code via:
bzr branch lp:lamson
Bazaar may ask you to login, but it should still give you the source. Refer to the "download instructions":/download.html for more information. h2. What features are planned for the 1.0 release? I try to make "1.0" what most other people would consider 80% complete. This lets people get it and work with it, and then I can refine it for the 2.0 release with 100% of features people actually use. With this in mind, I'm planning on the 1.0 release to be based on the sample applications and my own applications and include the smallest set of features. The most important part of the 1.0 release will be good documentation and bug free high quality code. h2. What are Lamson's current features? Lamson is currently running a handful of sites and is almost ready for a 1.0 release. It has these high level features: * Very good bounce detection and analysis. * Spam blocking. * Get an application going quickly with the "lamson gen" command. * Full set of "self-documented commands":/docs/lamson_commands.html for managing and developing a lamson application. * Lots of "documentation":/docs/ with more to come. * Sample applications for you to base your work on. * Handle mail for arbitrary hosts and addresses using simple regex routing. * Process requests including full access to Python's complete MIME email libraries. * Absolutely awesome full conversion to Unicode including complete cleaning of incoming mail. * Routing requests in a standard web application framework style based on addresses. * Processing mail messages (requests) in either simple generic handlers, or in more complex and robust state machines. * Use any Python database storage you want. * Use of Jinja2 or Mako templates, with Jinja2 as the default. * Craft and send plain text or HTML email including attachments, with a great HTML mail generation API that even "knits" in CleverCSS. * Unit test helpers for having a conversation, checking spelling, etc. * Defer processing to Maildir queues for offline handling of bigger tasks. * Extensive logging and development tools for debugging your email applications. * Mutt configurations to fake out mutt so that it talks to your server. * 100% code coverage in the unit tests. h2. Isn't Postfix/Sendmail/Exim Faster? That is a tough question to answer actually. If all you need to do is receive and deliver email then a well established traditional email server like Postfix or Exim (please don't use Sendmail) is the way to go. Hands down these servers are the fastest and the best at this job. However, if you need to actually do something smart with your email, like manage many mailing lists or handle support requests, then these servers are definitely slower. The reason is they require that you configure them to take messages they've already received and hand them to a separate process like a Perl, Python, or Ruby script. This separate process then has to parse the message *again*, do its job without stepping on any other processes that might be running (that means locks), and then send response messages back to the server for even more SMTP parsing. With the triple and sometimes quadruple MIME parsing, the heavy weight processes, the difficult to manage locking, and the additional configuration headaches, there's no way traditional mail servers beat Lamson in speed. Lamson only processes a message once, maybe twice if you defer to a queue. Once the message is parsed you get full access to Python immediately, without spawning a separate process. Even if you defer to a queue, the Lamson dequeue server stays resident and processes the queue without forking. You can even run many dequeue servers on mulitple machines processing a shared Maildir if you need the extra processing. In the end, threads and function calls beats processes and pipes. h2. Why not use Sendmail's Milter? Sendmail has a protocol named "Milter" that lets you write a mail processing server that acts as a sort of "slave" to the sendmail process. This protocol is supported by at least Postfix as well, maybe other servers. Feel free to go try Milter. When you're done trying to figure out the protocol from the dense C code, configure the m4 macros, find a decent milter protocol library that doesn't involve installing sendmail, and debugging the final setup, then you can come back and have it easy with Lamson. h2. Why does Lamson send messages to a relay host? Lamson doesn't have to deliver to a relay host, but it is a smarter more practical use of the technology. Lamson is written in Python and does actually run slower than the established mail servers. In addition, Lamson is hopefully doing something more than just routing email around to people. It is probably processing messages, crafting replies, querying databases, hitting REST interfaces, and all the other things you'd want to do with a modern application. This takes time and resources and are probably more valuable operations than just simple delivery. For this reason, you want to use a dumb workhorse like Postfix to do your actual delivery, and reserve the smart processing that has value for Lamson. h2. What about security?! Shouldn't Lamson be 20 processes? Have you ever asked why other mail servers are a bundle of a billion processes? Why have one server receiving mail, another routing it, and another handing it to users? The answer is back in the 1970's most mail was delivered to Unix users in their home directories or similar files that required special access rights to modify. Also in the 1970's special ports like 25 for SMTP required root access, which in the tiny Internet of the time meant that the server could be "trusted". These two realities of the time meant that to receive and deliver mail at least some part of the system had to run as root. To keep things safe, modern mail systems reduce the amount of time spent as the root user by separating their functionality into different processes. However, if you never have to deliver to a user, and all you ever do is process mail and talk to other servers like RDBMS, then why do you need all this privilege separation? Sites run just fine with systems running as one or two processes without the complexity of some illogical privilege separation getting in their way. To put this into perspective, imagine that you were writing a Django application and you were required to have a separate process for the HTTP layers, the view layer, the model layer, the HTTP responses, and the RDBMS access layers? Each one required a different user, a different configuration file, and you needed another process just to keep them all sane. All of this just so that if someone hacks into your HTTP server as root they supposedly can't cause any damage. Yet, they are on your server as root after all. In practice, you can run Lamson as a separate root process, and then use another "dequeue server" to do the real processing, if you feel you need that security. But, consider delaying that decision until you absolutely need it, because the security benefits aren't worth the development and deployment hassles. h2. How come nobody thought of this before? I don't know why, since it did seem kind of simple. There's at least one other project written in Perl called "qpsmtpd":http://smtpd.develooper.com/ that does something similar, and there may be more. If you know others feel free to "contact me":/contact.html and let me know. h2. Isn't [Insert Random Java Mail Server] actually the first mail "framework"? I get this quite frequently when I make the claim that Lamson is the first email framework, and it may be true that there was a framework out there before Lamson. The internet is a big place, so anything is possible. However, I looked really hard and I couldn't find a single *modern* mail framework. All that existed were servers I could use to build a framework. You see, around 2003 or 2004 the concept of "framework" changed. Before then all you needed was a server with an extension API named so that it rhymed with "Servlet". As long as your server provided a way to drop a class into the processing queue and let a programmer handle the request you could call that a framework. The usual end result for these *servers* is that you could use them to build a framework if you wanted, but what you'd get is affectionately called a "frankenstack". You'd grab an ORM from here, a template system from there, maybe a workflow engine, write a Maven or Ant script to manage it, and wire it all together with some lame secret sauce code you think gives you a competitive edge. Then along came the modern frameworks like Django and Rails that included everything you needed in a bundle that you could use right away. They had ORM, templating, routing, higher level request processing, email support, REST support, and anything else you might need for the 80% of your application you don't care about. Some people prefer less of these defaults, some people more, but nearly everyone who has to get a project done prefers more than just an extension API so they can build their own framework. Today if you try to claim "Apache James":http://james.apache.org/ is a framework you'd be wrong. I could *build* a framework with it, but I could just as easily build that same framework with Python, Ruby, sendmail and even postfix. James and friends are just servers, not frameworks. In fact, my experience with James and similar Java mail servers is they are much harder to use than aliases+pipes in Postfix. I now advocate that if your framework doesn't at least support data, views, and high level logic as first class entities then it's just a server. You don't have to use ORM, any particular templating, or Finite State Machines like Lamson does. You don't even have to settle on only one way to do data, views, and logic. You *must* at least support data, views, and logic out of the box so your user doesn't have to go shopping at "APIs-R-Us" just to use your gear. lamson-1.0pre11/doc/lamsonproject.org/output/docs/filtering_spam.html0000644000076500000240000002477111242713200025443 0ustar zedshawstaff LamsonProject: Filtering Spam With Lamson

Filtering Spam With Lamson

Lamson supports initial use of the SpamBayes spam filter library for filtering spam. What Lamson provides is a set of easy to use decorators that you attach to your state functions which indicate that you want spam filtered. It also uses the default SpamBayes configuration files and database formats as you configure, so if you have an existing SpamBayes setup you should be able to use it right away.

Using lamson.spam

Lamson gives you a simple decorator to place on any state functions that should block spam. Typically you do not want spam filtering on your entire application, since that would prevent legitimate registrations and put too much burden on your system. It’s better to put spam filtering on the “insider” parts, and to have confirmation emails on “outsider” pieces.

Instead, what you want is to indicate that your “choke points” are filtering spam using lamson.spam.spam_filter so that when a spam is received they are put into a “spam black hole”.

Here’s an trivial example where the user is in the POSTING state, and you want everything to work like normal, but if they spam then you flip them into a SPAMMING state.

@route(".+")
def SPAMMING(message):
    # the spam black hole
    pass

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter("run/spamdb", "run/.hammierc", "run/spam", next_state=SPAMMING)
def POSTING(message, **kw):
    print "Ham message received."
    ... 

The line to look at is obviously the spam_filter line, which tells Lamson that you will:

  1. Use the SpamBayes training database run/spamdb for the detection.
  2. Use the SpamBayes run/.hammierc file for your config (optional and ignored if it is not there).
  3. Use run/spam as the dumping ground for anything classified as spam.
  4. The next_state to transition to if they send a spam message. This is optional, but very helpful.

With this, the spam_filter then wraps your state function, and every message is fed to SpamBayes. If SpamBayes says it’s spam then Lamson will dump it into your run/spam and transition to SPAMMING *without running your POSTING state*.

Once you are in this new SPAMMING state (or any state you like) you can do whatever you want. You can leave them there, or you can have an external tool that let’s you un-block someone. Pretty much any spam handling scheme you want is available.

Since your spam is placed into a queue you can inspect it later and check for any accidentally miscategorized mail, then use the SpamBayes tools to retrain for the misdetection.

Lamson only classifies mail that is marked as actual spam by looking at the 'X-Spambayes-Classification’ header and seeing if it starts with 'spam’. If it is 'unsure’ or 'ham’ it will let it through.

Effectiveness

I’ve been running a variant of this since the middle of May 2009 and it works great. The code I run is a custom version that fits the weirdness of my email setup but the principles are the same. I’m currently using the above spam filtering, some gray listing, and a few other tricks to block most of my incoming spam.

With all the spam block measures I’ve managed to cut down my spam to about 2-3 a day out of about 100-200 I receive. The majority of the “spam” that gets through is actually email that’s classified as “unsure” which I then use to retrain SpamBayes to make it stronger.

However, that’s my personal server, so in the case of a Lamson application you’ll want to be careful that your spam blocking activities don’t prevent too much legitimate use.

Changing What “Spam” Means

You can also change how spam is determined by sub-classing lamson.spam.spam_filter and doing your own implementation of the spam method.

Using SpamBayes

An important point about SpamBayes is that it comes with all the command line tools you need to configure and train your database using a corpus of spam you might have. All Lamson needs to do is read this database to determine if it is spam or not.

With mutt, I save the message to “=spam”, which places the spam in Mail/spam along with all of the others. Then I run this command:

sb_mboxtrain.py -s ~/Mail/spam -d run/spamdb

This goes through the spam mailbox, and any emails that SpamBayes has not already classified get used for training.

SpamBayes comes with other commands you can read about on their site (if you can find it).

Autotraining

Lamson doesn’t support “autotraining” directly, since it’s not clear in each situation what is obviously spam. In my personal setup I know that any email not for registered users is obviously spam, so I can autotrain those.

If you want to implement autotraining for a part of your application, then look at the API for lamson.spam.Filter and simply use it in the right state function.

Configuration

Finally, the above sample code is not the best way to configure the spam filter. It’s better to put the configuration in config/settings.py and simply reference it from there.

In your config/settings.py put this:

SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}

Then change your handler code to be this:

from config.settings import SPAM

@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, ...):
   # this is the better way to do your config

With that you can then change up the configuration as needed in your deployments without having to change your code.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/filtering_spam.txt0000644000076500000240000001343011242713153025313 0ustar zedshawstaffTitle: Filtering Spam With Lamson Lamson supports initial use of the "SpamBayes":http://spambayes.sourceforge.net/ spam filter library for filtering spam. What Lamson provides is a set of easy to use decorators that you attach to your state functions which indicate that you want spam filtered. It also uses the default SpamBayes configuration files and database formats as you configure, so if you have an existing SpamBayes setup you should be able to use it right away. h2. Using lamson.spam Lamson gives you a simple decorator to place on any state functions that should block spam. Typically you do *not* want spam filtering on your entire application, since that would prevent legitimate registrations and put too much burden on your system. It's better to put spam filtering on the "insider" parts, and to have confirmation emails on "outsider" pieces. Instead, what you want is to indicate that your "choke points" are filtering spam using "lamson.spam.spam_filter":http://lamsonproject.org/docs/api/lamson.spam.spam_filter-class.html so that when a spam is received they are put into a "spam black hole". Here's an trivial example where the user is in the POSTING state, and you want everything to work like normal, but if they spam then you flip them into a SPAMMING state.
@route(".+")
def SPAMMING(message):
    # the spam black hole
    pass

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter("run/spamdb", "run/.hammierc", "run/spam", next_state=SPAMMING)
def POSTING(message, **kw):
    print "Ham message received."
    ... 
The line to look at is obviously the @spam_filter@ line, which tells Lamson that you will: # Use the SpamBayes training database @run/spamdb@ for the detection. # Use the SpamBayes @run/.hammierc@ file for your config (optional and ignored if it is not there). # Use @run/spam@ as the dumping ground for anything classified as spam. # The next_state to transition to if they send a spam message. *This is optional, but very helpful.* With this, the @spam_filter@ then wraps your state function, and every message is fed to SpamBayes. If SpamBayes says it's spam then Lamson will dump it into your @run/spam@ and transition to SPAMMING *without running your POSTING state*. Once you are in this new @SPAMMING@ state (or any state you like) you can do whatever you want. You can leave them there, or you can have an external tool that let's you un-block someone. Pretty much any spam handling scheme you want is available. Since your spam is placed into a queue you can inspect it later and check for any accidentally miscategorized mail, then use the SpamBayes tools to retrain for the misdetection. bq. Lamson only classifies mail that is marked as actual spam by looking at the 'X-Spambayes-Classification' header and seeing if it starts with 'spam'. If it is 'unsure' or 'ham' it will let it through. h2. Effectiveness "I've":http://zedshaw.com/ been running a variant of this since the middle of May 2009 and it works great. The code I run is a custom version that fits the weirdness of my email setup but the principles are the same. I'm currently using the above spam filtering, some gray listing, and a few other tricks to block most of my incoming spam. With all the spam block measures I've managed to cut down my spam to about 2-3 a day out of about 100-200 I receive. The majority of the "spam" that gets through is actually email that's classified as "unsure" which I then use to retrain SpamBayes to make it stronger. However, that's my personal server, so in the case of a Lamson application you'll want to be careful that your spam blocking activities don't prevent too much legitimate use. h2. Changing What "Spam" Means You can also change how spam is determined by sub-classing "lamson.spam.spam_filter":http://lamsonproject.org/docs/api/lamson.spam.spam_filter-class.html and doing your own implementation of the @spam@ method. h2. Using SpamBayes An important point about SpamBayes is that it comes with all the command line tools you need to configure and train your database using a corpus of spam you might have. All Lamson needs to do is read this database to determine if it is spam or not. With mutt, I save the message to "=spam", which places the spam in Mail/spam along with all of the others. Then I run this command:
sb_mboxtrain.py -s ~/Mail/spam -d run/spamdb
This goes through the spam mailbox, and any emails that SpamBayes has *not* already classified get used for training. SpamBayes comes with other commands you can "read about":http://spambayes.sourceforge.net/docs.html on their site (if you can find it). h2. Autotraining Lamson doesn't support "autotraining" directly, since it's not clear in each situation what is obviously spam. In my personal setup I know that any email not for registered users is obviously spam, so I can autotrain those. If you want to implement autotraining for a part of your application, then look at the API for "lamson.spam.Filter":http://lamsonproject.org/docs/api/lamson.spam.Filter-class.html and simply use it in the right state function. h2. Configuration Finally, the above sample code is not the best way to configure the spam filter. It's better to put the configuration in @config/settings.py@ and simply reference it from there. In your @config/settings.py@ put this:
SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}
Then change your handler code to be this:
from config.settings import SPAM

@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, ...):
   # this is the better way to do your config
With that you can then change up the configuration as needed in your deployments without having to change your code. lamson-1.0pre11/doc/lamsonproject.org/output/docs/getting_started.html0000644000076500000240000012475211242713463025642 0ustar zedshawstaff LamsonProject: Getting Started With Lamson

Getting Started With Lamson

Lamson is designed to work like modern web application frameworks like Django, TurboGears, ASP.NET, Ruby on Rails, and whatever PHP is using these days. At every design decision Lamson tries to emulate terminology and features found in these frameworks. This Getting Started document will help you get through that terminology, get you started running your first lamson application, and walk you through the code you should read.

In total it should take you about 30 minutes to an hour to complete. If you just want to try Lamson, at least go through the 30 second introduction given first.

The 30 Second Introduction

If you have Python and easy_install already, then try this out:

$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc

You now have a working base Lamson setup ready for you to work on with the following installed:

  • Lamson and all dependencies (Jinja2, nosetests)
  • Code for your project in mymailserver. Look in app/handlers and config/settings.py.
  • Two initial tests that verify your server is not an open relay and forwards mail in tests/handlers/open_relay_tests.py.
  • A “logger” server running on port 8825 that dumps all of its mail into the run/queue maildir.
  • A config script for mutt (muttrc) that you can use to inspect the run/queue and also send mail using Lamson’s send command.

When you’re in mutt during the above test run, try sending an email. The included muttrc is configured to use the run/queue as the mail queue, and to use the lamson sendmail command to deliver the mail. This tricks mutt into interacting directly with your running Lamson server, so you can test the thing with a real mail client and see how it will work without having to actually deploy the server.

Finally, if you wanted to stop all of above you would do:

$ lamson stop -ALL run

Which tells Lamson to stop all processes that have a .pid file in the run directory.

Important Terminology

If you are an old SMTP guru and/or you’ve never written a web application with a modern web framework, then some of the terminology used in Lamson may seem confusing. Other terms may just confuse you or scare you because they sound complicated. I tried my best to make the concepts used in Lamson understandable and the code that implements them easy to read. In fact, you could probably read the code to Lamson in an evening and understand how everything works.

Experience has taught me that nobody reads the code, even if it is small. Therefore, here are the most important concepts you should know to get a grasp of Lamson and how it works.

  • MVC — Model View Controller is a design methodology used in web application frameworks where the data (model), presentation (view), and logic (controller) layers of the application are strictly separated.
  • FSM — Lamson uses the concept of a Finite State Machine to control how handlers execute. Each time it runs it will perform an action based on what it is send and what it was doing last. FSM in computer science class are overly complex, but in Lamson they are as easy to use as a return statement.
  • Template — Lamson generates the bodies of its messages using Templates, which are text files that have parts that get replaced with variables you pass in. Templates are converted to their final form with a process called rendering.
  • Relay — The relay for a Lamson server is where Lamson delivers its messages. Usually the Relay is a smart tougher server that’s not as smart, but very good at delivering mail. Lamson can also be run as a Relay for testing purposes.
  • Receiver — Lamson typically runs as the Receiver of email. If you are familiar with a web application setup, then Lamson is the inverse. Instead of Lamson runing “behind” an Apache or Nginx server, Lamson runs “in front” of an SMTP server like Postfix. It listens on port 25, handles the mail it should, and forwards the rest to the Relay. This makes Lamson much more of a Proxy or filter server.
  • Queue — Lamson can also do all of its processing off a queue. In this setup you would have your normal mail server dump all mail to a maildir queue, and then tell Lamson to process messages out of there. This can be combined with the usual Receiver+Relay configuration for processing messages that might take a long time.
  • Maildir — A standard created for the qmail project with stores mail in a directory such that you can access the mail atomically and store it on a shared disk without conflicts or locking.

Managing Your Server

Your Lamson application is now running inside the Lamson Python server. This is a very simple server based on Python’s smtpd and asyncore libraries.

If you want to know more about how it operates, take a look at the lamson/server.py file in the source distribution.

You’ll need to use a few Lamson commands to manage the server. You already experienced them in the 30 second introduction, and you can review them all or see them by using the lamson help command.

Right now you have Lamson running on port 8823 and a “Lamson logger” running on 8825. This means that your lamson server (port 8823) will forward its messages to the logger (port 8825) thinking it’s your real relay server. The truth is the logger just logs its messages to logs/logger.log and dumps it into run/queue so you can inspect the results.

Before we learn how to manage them and what they do, open up the config/settings.py file and take a look:

from app.model import table
import logging

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

database_config = {
    "metadata" : table.metadata,
    "url" : 'sqlite:///app/data/main.db',
    "log_level" : logging.DEBUG
}

handlers = ['app.handlers.sample']

router_defaults = {'host': 'test\\.com'}

template_config = {'dir': 'app', 'module': 'templates'}

Your file probably has some comments telling you what these do, but it’s important to understand how they work.

First, this file is just plain old Python variables. It is loaded by one of two other files in your config directory: config/boot.py or config/testing.py. The config/boot.py file is started whenever you use the lamson start command and its job is to read the config/settings.py and start all the services you need, then assign them as variables back to config.settings so your handlers can get at them. The config/testing.py is almost the same, except it configures config.settings so that your unit tests can run without any problems. Typically this means setting the spell checker and not starting the real server.

Lamson can load any boot script you like, see Deferred Processing To Queues for an example of using this to make a queue processor.

The important thing to understand about this setup (where a boot file reads settings.py and then configures config.settings) that it makes it easy for you to change Lamson’s operations or start additional services you need and configure them. For the most part you won’t need to touch boot.py or testing.py until you need to add some new service, change the template library you want to use, setup a different database ORM, etc. Until then just ignore it.

settings.py Variables

The receiver_config variable is used by the lamson start command to figure out where to listen for incoming SMTP connections. In a real installation this would be port 25 on your external IP address. It’s where the internet talks to your server.

The relay_config setting is used by Lamson to figure out where to forward message replies (responses) for real delivery. Normally this would be a “smart host” running a more established server like Postfix or Exim to do the grunt work of delivering to the final recipients.

The handlers variable lists the modules (not files) of the handlers you want to load. Simply put them here and they’ll be loaded, even the lamson.handlers modules will work here too.

The router_defaults are for the lamson.routing.Router class and configure the default routing regular expressions you plan on using. Typically you’ll at least configure the host regular expression since that is used in every route and shouldn’t change too often.

Finally, template_config contains the configuration values for the templating system you’ll be using. Lamson supports either Mako or Jinja2, but defaults to Jinja2.

Looking At config/boot.py

Programmers need to know how everything works before they trust it, so let’s look at the config/boot.py file and see how these variables are used:

from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson.utils import configure_database
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

Don’t be afraid that you see this much Python, you normally wouldn’t touch this file unless it were to add your own services or to make a new version for a different configuration. For the most part, you can just edit the config/settings.py and go.

First you’ll see that config/boot.py sets up logging using the config/logging.conf file, which you can change to reconfigure how you want logs to be created.

Then it starts assigning variables to the config.settings module that it has imported at the top. This is important because after config.boot runs your lamson code and handlers will have access to all these services. You can get directly to the relay, receiver, database and anything else you need by simply doing:

from config import settings

After that config.boot sets up the settings.relay, settings.receiver, and settings.database. These three are used heavily in your own Lamson code, so knowing how to change them if you need to helps you later.

After this we configure the lamson.routing.Router to have your defaults, load up your handlers, and turn on RELOAD. Setting Router.RELOAD=True tell the Router to reload all the handlers for each request. Very handy when you are doing development since you don’t need to reload the server so often.

If you deploy to production, then you’ll want to set this to False since it’s a performance hit.

Finally, the config.boot does the job os loading the template system you’ll use, in this case Jinja2. Jinja2 and Mako use the same API so you can configure Mako here as well, as long as the object assigned to view.LOADER has the same API it will work.

Developing With Lamson

Now that you’ve received a thorough introduction to how to manage Lamson, and how it is configured, you can get into actually writing some code for it.

Before you begin, you should know that writing an application for a mail server can be a pain. The clients and servers that handle SMTP make a large number of assumptions based on how the world was back in 1975. Everything is on defined ports with defined command line parameters and the concept of someone pointing their mail client at a different server arbitrarily just doesn’t exist. The world of email is not like the web where you just take any old “client” and point it at any old server and start messing with it.

Lucky for you, Lamson has solved most of these problems and provides you with a bunch of handy development tools and tricks so you can work with your Lamson server without having to kill yourself in configuration hell.

Using Mutt

You probably don’t have another SMTP server running, and even if you did, it’d be a pain to configure it for development purposes. You’d have to setup aliases, new mail boxes, restart it all the time, and other annoyances.

For development, what we want is our own little private SMTP relay, and since Lamson can also deliver mail, that is what we get with the command:

$ lamson log

This tells Lamson to run as a “logging server”, which doesn’t actually deliver any mail. With this one command you have a server running on 8825 that takes every mail it receives and saves it to the run/queue Maildir and also logs it to logs/logger.log. It also logs the full protocol chat to logs/lamson.err so you can inspect it.

Lamson uses Maildir by default since it is the most reliable and fastest mail queue format available. It could also store mail messages to any queue supported by Python’s mailbox library. If you were adventurous you could also use a RDBMS, but that’s just silly.

You also have the file muttrc which is configured to trick mutt into talking to your running Lamson server, and then read mail out of the run/queue maildir that is filled in by the lamson log server. Let’s take a look:

set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"

Notice that it’s configured sendmail to be “sendmail -port 8823 -host 127.0.0.1” which is a special lamson sendmail command that knows how to talk to lamson and read the arguments and input that mutt gives to deliver a mail.

Why does Lamson need its own sendmail? Because you actually have to configure most mail server’s configuration files to change their ports before their sendmail command will use a different port. Yes, the average sendmail command line tool assumes that it is always talking to one and only one server on one and only one port for ever and all eternity. Without lamson sendmail you wouldn’t be able to send to an arbitrary server.

With this setup (lamson start ; lamson log ; mutt -F muttrc) you can now use your mutt client as a test tool for working with your application.

Stopping Lamson

The PID files are stored in the run directory. Here’s a sample session where I stop all the running servers:

$ ls -l run/*.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/log.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/smtp.pid

$ lamson stop -ALL run
Stopping processes with the following PID files: ['run/log.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 1693
Attempting to stop lamson at pid 1689

You can also pass other options to the stop command to just stop one server. Use lamson help -for stop to see all the options.

Starting Lamson Again

Hopefully you’ve been paying attention and have figured out how to restart lamson and the logging server. Just in case, here it is again:

$ lamson start
$ lamson log

You should also look in the logs/lamson.log file to see that it actually started. The other files in the logs directory contain messages dumped to various output methods (like Python’s stdout and stderr). Periodically, if the information you want is not in logs/lamson.log then it is probably in the other files.

You can change your logging configuration by editing the logging line your config/settings.py file.

Other Useful Commands

You should read the available commands documentation to get an overview, and you can also use lamson help to see them at any time.

send

The first useful command is lamson send, which lets you send mail to SMTP servers (not just Lamson) and watch the full SMTP protocol chatter. Here’s a sample:

$ lamson send -port 25 -host zedshaw.com -debug 1 \
    -sender tester@test.com -to zedshaw@zedshaw.com \
    -subject "Hi there" -body "Test body."
send: 'ehlo zedshawscomputer.local\r\n'
reply: '502 Error: command "EHLO" not implemented\r\n'
reply: retcode (502); Msg: Error: command "EHLO" not implemented
send: 'helo zedshawcomputer.local\r\n'
reply: '250 localhost.localdomain\r\n'
reply: retcode (250); Msg: localhost.localdomain
send: 'mail FROM:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'rcpt TO:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'data\r\n'
reply: '354 End data with .\r\n'
reply: retcode (354); Msg: End data with .
data: (354, 'End data with .')
send: 'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nSubject: Hi there\r\nFrom: tester@test.com\r\nTo: zedshaw@zedshaw.com\r\n\r\n.\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
data: (250, 'Ok')
send: 'quit\r\n'
reply: '221 Bye\r\n'
reply: retcode (221); Msg: Bye

Using this helps you debug your Lamson server by showing you the exact protocol sent between you and the server. It is also a useful SMTP server debug command by itself.

When you use the supplied muttrc you’ll be configured to use Lamson’s sendmail (not *send) command as your delivery command. This lets you use mutt as a complete development tool with minimal configuration.

queue

The lamson queue command lets you investigate and manipulate the run/queue (or any maildir). You can pop a message off, get a message by its key, remove a message by its key, count the messages,clear the queue, list keys in the queue. It gives you a lower level view of the queue than mutt would, and lets you manipulate it behind the scenes.

restart

Lamson does reload the code of your project when it receives a new request (probably too frequently), but if you change the config/settings.py file then you need to restart. Easiest way to do that is with the restart command.

Walking Through The Code

You should actually know quite a lot about how to run and mess with Lamson, so you’ll want to start writing code. Before you do, go check out the API Documentation and take a look around. This document will guide you through where everything is and how to write your first handler, but when you start going out on your own you’ll need a good set of reference material.

At the top level of your newly minted project you have these directories:

app -- Where the application code (handlers, templates, models) lives.
config -- You already saw everything in here.
logs -- Log files get put here.
run -- Stuff that would go in a /var/run like PID files and queues.
tests -- Unit tests for handlers, templates, and models.

Lamson expects all of these directories to be right there, so don’t get fancy and think you can move them around.

The first place to look is in the app directory, which has this:

app/__init__.py
app/data -- Data you want to keep around goes here.
app/handlers -- Lamson handlers go here.
app/model -- Any type of backend ORM models or other non-handler code.
app/templates -- Email templates.

You don’t technically have to store your data in app/data. You are free to put it anywhere you want, it’s just convenient for most situations to have it there.

Your app/model directory could have anything in it from simple modules for working various Maildir queues, to full blown SQLAlchemy configurations for your database. The only restriction is that you load them in the modules yourself (no magic here).

The app/templates directory can have any structure you want, and as you saw from the config.boot discussion it is just configured into the Jinja2 configuration as the default. If you have a lot of templates it might help to have them match your app/handlers layout in some logical way.

That only leaves your app/handlers directory:

app/handlers/__init__.py
app/handlers/sample.py

This is where the world gets started. If you look at your config.settings you’ll see this line:

handlers = ['app.handlers.sample']

Yep, that’s telling the lamson.routing.Router to load your app.handlers.sample module to kick it into gear. It really is as simple as just putting the file in that directory (in in sub-modules there) and then adding them to the handlers list.

You can also add handlers from modules outside of your app.handlers:

handlers = ['app.handlers.sample', 'lamson.handlers.log']

This installs the handler (lamson.handlers.log) that lamson uses to log every email it receives.

Writing Your Handler

This document is for getting started quickly, so going into the depths of the cool stuff you can do with Lamson handlers is outside the scope, but if you open the app/handlers/sample.py file and take a look you’ll how a handler is structured.

Since Lamson is changing so much the contents of the file aren’t included in this document. You’ll have to open it and take a look.

At the top of the file you should see your typical import statements:

import logging
from lamson.routing import route, route_like, stateless
from config.settings import relay, database
from lamson import view

Notice that we include elements from the lamson.routing that are decorators we use to configure a route. Then you’ll see that we’re getting that settings.relay and settings.database we configured in the previous sections. Finally we bring in the lamson.view module directory to make rendering templates into email messages a lot easier.

Now take a look at the rest of the file and you’ll how a handler is structured:

  1. Each state is a separate function in CAPS. It doesn’t have to be, it just looks better.
  2. Above each state function is a route, route_like, or stateless decorator to configure how lamson.routing.Router uses it.
  3. The route decorator takes a pattern and then regex keyword arguments to fill it in. The words in the pattern string are replaced in the final more complex routing regex by the keyword arguments after. However, if you want to use regex directly you can, route just needs a string that eventually becomes a regex.
  4. A state function changes state by returning the next function to call. You want to go to the RUNNING state, just return RUNNING.
  5. If any state function throws an error it will go into the ERROR state, so if you make a state handler named ERROR it will get called on the next event and can recover.
  6. If you want to run a state on this event rather than wait to have it run on the next, then simple call it and return what it returns. So to have RUNNING go now, just do return RUNNING(message, ...) and it will work.
  7. If a state has the same regex as another state, just use route_like to say that.
  8. If you have a stateless decorator after a route or route_like, then that handler will run for all addresses that match, not just if this handler is in that state.

That is pretty much the entire complexity of how you write a handler. You setup routes, and return the next step in your conversation as the next function to run. The lamson.routing.Router then takes each message it receives and runs it through a processing loop handing it to your states and handlers.

How States Are Run

The best way to see how states are processed is to look at the Router code that does it:

    def deliver(self, message):
        if self.RELOAD: self.reload()

        called_count = 0

        for functions, matchkw in self.match(message['to']):
            to_call = []
            in_state_found = False

            for func in functions:
                if lamson_setting(func, 'stateless'):
                    to_call.append(func)
                elif not in_state_found and self.in_state(func, message):
                    to_call.append(func)
                    in_state_found = True

            called_count += len(to_call)

            for func in to_call:
                if lamson_setting(func, 'nolocking'):
                    self.call_safely(func, message,  matchkw)
                else:
                    with self.call_lock:
                        self.call_safely(func, message, matchkw)

        if called_count == 0:
            if self.UNDELIVERABLE_QUEUE:
                LOG.debug("Message to %r from %r undeliverable, putting in undeliverable queue.",
                          message['to'], message['from'])
                self.UNDELIVERABLE_QUEUE.push(message)
            else:
                LOG.debug("Message to %r from %r didn't match any handlers.",
                          message['to'], message['from'])

What this does is take all the handlers you’ve loaded, and then finds which handlers have a state function that matches the current message. It then goes through each potential match, and determines which of all the matching state functions is “in that state”. This means that, even though you have six state functions that answer to “(list_name)-(action)@(host)” only the one that matches the users current state (say PENDING) will be called next. As it goes through these functions it also loads up any that are marked “stateless” so they can be called as well.

Finally, it just calls them in order. If the message results in no methods to call, then it will take the message and tell you this, or put it into an UNDELIVERABLE_QUEUE for you to review it later.

Slight design criticism: Currently the order of these calls is fairly deterministic, but you can’t rely on it. It’s also not clear if all matching states should run, or just the first. It currently only runs the first match, but it might be better to run each match from each handler. Suggestions welcome on this.

Debugging Routes

In the old way of doing routing you would edit a large table of “routes” in your config/settings.py file and then that told Lamson how to run. The problem with this is it was too hard to maintain and too hard to indicate that different states needed a different route.

The new setup is great because all your routing for each handler module is right there, and it’s easy to see what will cause a particular state function to go off.

What sucks about the new setup is that you can’t find out what all the routes are doing globally in one place. That’s where lamson routes comes in. Simply run that command and you’ll get a debug dump of all the full routing regex and the functions and modules they belong to:

Routing ORDER:  ['^(?P<address>.+)@(?P<host>test\\.com)$']
Routing TABLE: 
---
'^(?P<address>.+)@(?P<host>test\\.com)$':  app.handlers.sample.START  app.handlers.sample.NEW_USER
   app.handlers.sample.END  app.handlers.sample.FORWARD  
---

This is telling you which regex is matched first, then what those regex are mapped to. This is very handy as you can copy-paste that regex right into a python shell and then play with it to see if it would match what you want.

You can also pass in an email address to the -test option and it will tell you what routes would match and which functions that will call:

osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 

If you’re working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it’ll take an email address and tell you all the routes that match it.

THREADING!

Lamson takes a lighter approach to how it runs. It assumes that most of the time you want lamson to keep itself sane with minimal locking, and that you want each of your state functions to run in a thread lock that prevents others from stepping on your operations. In 95% of the cases, this is what you want.

To accomplish this, Lamson’s router will acquire an internal lock for operations that change its state, and a separate lock before it calls each state function. Since multiple state functions run inside each thread, but one thread handles each message, you’ll get multiple processing, but each state won’t step on other states in the system.

However, it’s those 5% of the times that will kill your application, and if you know what you’re doing, you should be able to turn this off. In order to tell the Router not to lock your state function, simply decorate it with nolocking and Lamson will skip the locking and just run your state raw. This means that other threads will run potentially stepping on your execution, so you must do your own locking.

Now, don’t think that slapping a nolocking on your state functions is some magic cure for performance issues. You only ever want to do this if you really know your stuff, and you know how to make that operation faster with better controlled locking.

The reality is, if you have an operation that takes so long it blocks everything else, then you are doing it wrong by trying to do it all in your state function. You should change your design so that this handler drops the message into a lamson.queue.Queue and that another Lamson server reads messages out of that to do the long running processing.

Using queues and separate Lamson servers you can solve most of your processing issues without a lot of thread juggling and process locking. In fact, since Lamson uses maildir queues by default you can even spread these processors out to multiple machines reading off a shared disk and everything will be just fine.

But, since programmers will always want to just try turning off the locking, Lamson supports the nolocking decorator. Use with care.

What’s In A Unit Test

Writing unit tests is way outside the scope of this document, but you should read up on using nosetests, testunit, and you should look at lamson.testing for a bunch of helper functions. Also look in the generated tests directory to see some examples.

Spell Checking Your Email Templates

Another big help is that Lamson has support for PyEnchant so you can spell check your templates. You can use lamson.testing.spelling function in your unit tests.

Installing PyEnchant is kind of a pain, but the trick is to get the dictionary you want and put it in your ~/.enchant/myspell directory. You’ll also want to open the config/testing.py file and uncomment the lines at the bottom that tell PyEnchant where to find the enchant so (dylib).

PyEnchant is kind of hard to use, so if you have suggestions on a better Python spell checking lib for unit tests please let me know.

Spam Filtering For Free

Lamson comes with the lamson.spam module which supports SpamBayes spam filtering system.

Read the document on Filtering Spam With Lamson to get a full set of instructions on using the spam filtering features.

Other Examples

Next you’ll want to sink your teeth in a bigger example. Go grab the source distribution .tar.gz and extract it so you can get at the examples:

$ tar -xzvf lamson-VERSION.tar.gz
$ cd lamson-VERSION
$ cd examples/osb

You are now in the osb example that is running on oneshotblog.com. Using what you’ve learned so far you can start reviewing the code and finding out how a working example operates.

Getting Help

As you work through this documentation, send your questions to me and I’ll try to help you. You can also join the lamson@librelist.com mailing list and get help from other Lamson users.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/getting_started.txt0000644000076500000240000010461711242713461025511 0ustar zedshawstaffFrom: Zed Title: Getting Started With Lamson Lamson is designed to work like modern web application frameworks like Django, TurboGears, ASP.NET, Ruby on Rails, and whatever PHP is using these days. At every design decision Lamson tries to emulate terminology and features found in these frameworks. This Getting Started document will help you get through that terminology, get you started running your first lamson application, and walk you through the code you should read. In total it should take you about 30 minutes to an hour to complete. If you just want to try Lamson, at least go through the 30 *second* introduction given first. h2. The 30 Second Introduction If you have Python and "easy_install":http://peak.telecommunity.com/DevCenter/EasyInstall already, then try this out:
$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc
You now have a working base Lamson setup ready for you to work on with the following installed: * Lamson and all dependencies (Jinja2, nosetests) * Code for your project in mymailserver. Look in app/handlers and config/settings.py. * Two initial tests that verify your server is not an open relay and forwards mail in tests/handlers/open_relay_tests.py. * A "logger" server running on port 8825 that dumps all of its mail into the run/queue maildir. * A config script for mutt (muttrc) that you can use to inspect the run/queue *and* also send mail using Lamson's *send* command. When you're in mutt during the above test run, try sending an email. The included muttrc is configured to use the run/queue as the mail queue, and to use the @lamson sendmail@ command to deliver the mail. This tricks mutt into interacting directly with your running Lamson server, so you can test the thing with a real mail client and see how it will work without having to actually deploy the server. Finally, if you wanted to stop all of above you would do:
$ lamson stop -ALL run
Which tells Lamson to stop all processes that have a .pid file in the @run@ directory. h2. Important Terminology If you are an old SMTP guru and/or you've never written a web application with a modern web framework, then some of the terminology used in Lamson may seem confusing. Other terms may just confuse you or scare you because they sound complicated. I tried my best to make the concepts used in Lamson understandable and the code that implements them easy to read. In fact, you could probably read the code to Lamson in an evening and understand how everything works. Experience has taught me that nobody reads the code, even if it is small. Therefore, here are the most important concepts you should know to get a grasp of Lamson and how it works. * MVC(Model View Controller) -- Model View Controller is a design methodology used in web application frameworks where the data (model), presentation (view), and logic (controller) layers of the application are strictly separated. * FSM(Finite State Machine) -- Lamson uses the concept of a Finite State Machine to control how handlers execute. Each time it runs it will perform an action based on what it is send *and* what it was doing last. FSM in computer science class are overly complex, but in Lamson they are as easy to use as a @return@ statement. * Template -- Lamson generates the bodies of its messages using Templates, which are text files that have parts that get replaced with variables you pass in. Templates are converted to their final form with a process called *rendering*. * Relay -- The *relay* for a Lamson server is where Lamson delivers its messages. Usually the Relay is a smart tougher server that's not as smart, but very good at delivering mail. Lamson can also be run as a Relay for testing purposes. * Receiver -- Lamson typically runs as the Receiver of email. If you are familiar with a web application setup, then Lamson is the inverse. Instead of Lamson runing "behind" an Apache or Nginx server, Lamson runs "in front" of an SMTP server like Postfix. It listens on port 25, handles the mail it should, and forwards the rest to the Relay. This makes Lamson much more of a Proxy or filter server. * Queue -- Lamson can also do all of its processing off a queue. In this setup you would have your normal mail server dump all mail to a maildir queue, and then tell Lamson to process messages out of there. This can be combined with the usual Receiver+Relay configuration for processing messages that might take a long time. * Maildir -- A standard created for the qmail project with stores mail in a directory such that you can access the mail atomically and store it on a shared disk without conflicts or locking. h2. Managing Your Server Your Lamson application is now running inside the Lamson Python server. This is a very simple server based on Python's "smtpd":http://docs.python.org/library/smtpd.html and "asyncore":http://docs.python.org/library/asyncore.html libraries. bq. If you want to know more about how it operates, take a look at the @lamson/server.py@ file in the source distribution. You'll need to use a few Lamson commands to manage the server. You already experienced them in the 30 second introduction, and you can review "them all":/docs/lamson_commands.html or see them by using the @lamson help@ command. Right now you have Lamson running on port 8823 and a "Lamson logger" running on 8825. This means that your lamson server (port 8823) will forward its messages to the logger (port 8825) thinking it's your real relay server. The truth is the logger just logs its messages to logs/logger.log and dumps it into run/queue so you can inspect the results. Before we learn how to manage them and what they do, open up the @config/settings.py@ file and take a look:
from app.model import table
import logging

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

database_config = {
    "metadata" : table.metadata,
    "url" : 'sqlite:///app/data/main.db',
    "log_level" : logging.DEBUG
}

handlers = ['app.handlers.sample']

router_defaults = {'host': 'test\\.com'}

template_config = {'dir': 'app', 'module': 'templates'}
Your file probably has some comments telling you what these do, but it's important to understand how they work. First, this file is just plain old Python variables. It is loaded by one of two other files in your config directory: @config/boot.py@ or @config/testing.py@. The @config/boot.py@ file is started whenever you use the @lamson start@ command and its job is to read the @config/settings.py@ and start all the services you need, then assign them as variables back to @config.settings@ so your handlers can get at them. The @config/testing.py@ is almost the same, except it configures @config.settings@ so that your unit tests can run without any problems. Typically this means setting the spell checker and *not* starting the real server. bq. Lamson can load any boot script you like, see "Deferred Processing To Queues":/docs/deferred_processing_to_queues.html for an example of using this to make a queue processor. The important thing to understand about this setup (where a boot file reads settings.py and then configures @config.settings@) that it makes it easy for you to change Lamson's operations or start additional services you need and configure them. For the most part you won't need to touch @boot.py@ or @testing.py@ until you need to add some new service, change the template library you want to use, setup a different database ORM, etc. Until then just ignore it. h2. settings.py Variables The @receiver_config@ variable is used by the _lamson start_ command to figure out where to listen for incoming SMTP connections. In a real installation this would be port *25* on your external IP address. It's where the internet talks to your server. The @relay_config@ setting is used by Lamson to figure out where to forward message replies (responses) for real delivery. Normally this would be a "smart host" running a more established server like "Postfix":http://www.postfix.org/ or "Exim":http://www.exim.org/ to do the grunt work of delivering to the final recipients. The @handlers@ variable lists the modules (not files) of the handlers you want to load. Simply put them here and they'll be loaded, even the "lamson.handlers":http://lamsonproject.org/docs/api/lamson.handlers-module.html modules will work here too. The @router_defaults@ are for the "lamson.routing.Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html class and configure the default routing regular expressions you plan on using. Typically you'll at least configure the @host@ regular expression since that is used in every route and shouldn't change too often. Finally, @template_config@ contains the configuration values for the templating system you'll be using. Lamson supports either Mako or Jinja2, but defaults to Jinja2. h2. Looking At config/boot.py Programmers need to know how everything works before they trust it, so let's look at the _config/boot.py_ file and see how these variables are used:
from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson.utils import configure_database
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

settings.database = configure_database(settings.database_config, also_create=False)

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

bq. Don't be afraid that you see this much Python, you normally wouldn't touch this file unless it were to add your own services or to make a new version for a different configuration. For the most part, you can just edit the @config/settings.py@ and go. First you'll see that @config/boot.py@ sets up logging using the @config/logging.conf@ file, which you can change to reconfigure how you want logs to be created. Then it starts assigning variables to the config.settings module that it has imported at the top. This is important because after @config.boot@ runs your lamson code and handlers will have access to all these services. You can get directly to the relay, receiver, database and anything else you need by simply doing:
from config import settings
After that @config.boot@ sets up the @settings.relay@, @settings.receiver@, and @settings.database@. These three are used heavily in your own Lamson code, so knowing how to change them if you need to helps you later. After this we configure the @lamson.routing.Router@ to have your defaults, load up your handlers, and turn on RELOAD. Setting @Router.RELOAD=True@ tell the Router to reload all the handlers for each request. Very handy when you are doing development since you don't need to reload the server so often. bq. If you deploy to production, then you'll want to set this to False since it's a performance hit. Finally, the @config.boot@ does the job os loading the template system you'll use, in this case Jinja2. Jinja2 and Mako use the same API so you can configure Mako here as well, as long as the object assigned to view.LOADER has the same API it will work. h1. Developing With Lamson Now that you've received a thorough introduction to how to manage Lamson, and how it is configured, you can get into actually writing some code for it. Before you begin, you should know that writing an application for a mail server can be a pain. The clients and servers that handle SMTP make a large number of assumptions based on how the world was back in 1975. Everything is on defined ports with defined command line parameters and the concept of someone pointing their mail client at a different server arbitrarily just doesn't exist. The world of email is not like the web where you just take any old "client" and point it at any old server and start messing with it. Lucky for you, Lamson has solved most of these problems and provides you with a bunch of handy development tools and tricks so you can work with your Lamson server without having to kill yourself in configuration hell. h2. Using Mutt You probably don't have another SMTP server running, and even if you did, it'd be a pain to configure it for development purposes. You'd have to setup aliases, new mail boxes, restart it all the time, and other annoyances. For development, what we want is our own little private SMTP relay, and since Lamson can also deliver mail, that is what we get with the command:
$ lamson log
This tells Lamson to run as a "logging server", which doesn't actually deliver any mail. With this one command you have a server running on 8825 that takes every mail it receives and saves it to the @run/queue@ Maildir and also logs it to @logs/logger.log@. It also logs the full protocol chat to @logs/lamson.err@ so you can inspect it. bq. Lamson uses Maildir by default since it is the most reliable and fastest mail queue format available. It could also store mail messages to any queue supported by Python's "mailbox":http://docs.python.org/library/mailbox.html library. If you were adventurous you could also use a RDBMS, but that's just silly. You also have the file @muttrc@ which is configured to trick mutt into talking to *your* running Lamson server, and then read mail out of the @run/queue@ maildir that is filled in by the @lamson log@ server. Let's take a look:
set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
Notice that it's configured sendmail to be "sendmail -port 8823 -host 127.0.0.1" which is a special @lamson sendmail@ command that knows how to talk to lamson and read the arguments and input that mutt gives to deliver a mail. bq. Why does Lamson need its own sendmail? Because you actually have to configure most mail server's configuration files to change their ports before their *sendmail command* will use a different port. Yes, the average sendmail command line tool assumes that it is always talking to one and only one server on one and only one port for ever and all eternity. Without @lamson sendmail@ you wouldn't be able to send to an arbitrary server. With this setup (@lamson start@ ; @lamson log@ ; @mutt -F muttrc@) you can now use your mutt client as a test tool for working with your application. h2. Stopping Lamson The PID(Process ID) files are stored in the @run@ directory. Here's a sample session where I stop all the running servers:
$ ls -l run/*.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/log.pid
-rw-r--r--  1 zedshaw  staff  5 May 16 16:41 run/smtp.pid

$ lamson stop -ALL run
Stopping processes with the following PID files: ['run/log.pid', 'run/smtp.pid']
Attempting to stop lamson at pid 1693
Attempting to stop lamson at pid 1689
You can also pass other options to the stop command to just stop one server. Use _lamson help -for stop_ to see all the options. h2. Starting Lamson Again Hopefully you've been paying attention and have figured out how to restart lamson and the logging server. Just in case, here it is again:
$ lamson start
$ lamson log
You should also look in the logs/lamson.log file to see that it actually started. The other files in the logs directory contain messages dumped to various output methods (like Python's stdout and stderr). Periodically, if the information you want is not in logs/lamson.log then it is probably in the other files. bq. You can change your logging configuration by editing the logging line your config/settings.py file. h2. Other Useful Commands You should read the "available commands":/docs/lamson_commands.html documentation to get an overview, and you can also use _lamson help_ to see them at any time. h2. send The first useful command is _lamson send_, which lets you send mail to SMTP servers (not just Lamson) and watch the full SMTP protocol chatter. Here's a sample:
$ lamson send -port 25 -host zedshaw.com -debug 1 \
    -sender tester@test.com -to zedshaw@zedshaw.com \
    -subject "Hi there" -body "Test body."
send: 'ehlo zedshawscomputer.local\r\n'
reply: '502 Error: command "EHLO" not implemented\r\n'
reply: retcode (502); Msg: Error: command "EHLO" not implemented
send: 'helo zedshawcomputer.local\r\n'
reply: '250 localhost.localdomain\r\n'
reply: retcode (250); Msg: localhost.localdomain
send: 'mail FROM:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'rcpt TO:\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'data\r\n'
reply: '354 End data with .\r\n'
reply: retcode (354); Msg: End data with .
data: (354, 'End data with .')
send: 'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nSubject: Hi there\r\nFrom: tester@test.com\r\nTo: zedshaw@zedshaw.com\r\n\r\n.\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
data: (250, 'Ok')
send: 'quit\r\n'
reply: '221 Bye\r\n'
reply: retcode (221); Msg: Bye
Using this helps you debug your Lamson server by showing you the exact protocol sent between you and the server. It is also a useful SMTP server debug command by itself. bq. When you use the supplied muttrc you'll be configured to use Lamson's sendmail (not *send) command as your delivery command. This lets you use mutt as a complete development tool with minimal configuration. h2. queue The _lamson queue_ command lets you investigate and manipulate the run/queue (or any maildir). You can pop a message off, get a message by its key, remove a message by its key, count the messages,clear the queue, list keys in the queue. It gives you a lower level view of the queue than mutt would, and lets you manipulate it behind the scenes. h2. restart Lamson does reload the code of your project when it receives a new request (probably too frequently), but if you change the @config/settings.py@ file then you need to restart. Easiest way to do that is with the restart command. h2. Walking Through The Code You should actually know quite a lot about how to run and mess with Lamson, so you'll want to start writing code. Before you do, go check out the "API Documentation":/docs/api/ and take a look around. This document will guide you through where everything is and how to write your first handler, but when you start going out on your own you'll need a good set of reference material. At the top level of your newly minted project you have these directories:
app -- Where the application code (handlers, templates, models) lives.
config -- You already saw everything in here.
logs -- Log files get put here.
run -- Stuff that would go in a /var/run like PID files and queues.
tests -- Unit tests for handlers, templates, and models.
Lamson expects all of these directories to be right there, so don't get fancy and think you can move them around. The first place to look is in the app directory, which has this:
app/__init__.py
app/data -- Data you want to keep around goes here.
app/handlers -- Lamson handlers go here.
app/model -- Any type of backend ORM models or other non-handler code.
app/templates -- Email templates.
You don't technically *have* to store your data in app/data. You are free to put it anywhere you want, it's just convenient for most situations to have it there. Your @app/model@ directory could have anything in it from simple modules for working various Maildir queues, to full blown SQLAlchemy configurations for your database. The only restriction is that you load them in the modules yourself (no magic here). The @app/templates@ directory can have any structure you want, and as you saw from the @config.boot@ discussion it is just configured into the Jinja2 configuration as the default. If you have a lot of templates it might help to have them match your @app/handlers@ layout in some logical way. That only leaves your @app/handlers@ directory:
app/handlers/__init__.py
app/handlers/sample.py
This is where the world gets started. If you look at your @config.settings@ you'll see this line:
handlers = ['app.handlers.sample']
Yep, that's telling the "lamson.routing.Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html to load your @app.handlers.sample@ module to kick it into gear. It really is as simple as just putting the file in that directory (in in sub-modules there) and then adding them to the handlers list. You can also add handlers from modules outside of your @app.handlers@:
handlers = ['app.handlers.sample', 'lamson.handlers.log']
This installs the handler ("lamson.handlers.log":http://lamsonproject.org/docs/api/lamson.handlers.log-module.html) that lamson uses to log every email it receives. h2. Writing Your Handler This document is for getting started quickly, so going into the depths of the cool stuff you can do with Lamson handlers is outside the scope, but if you open the _app/handlers/sample.py_ file and take a look you'll how a handler is structured. bq. Since Lamson is changing so much the contents of the file aren't included in this document. You'll have to open it and take a look. At the top of the file you should see your typical import statements:
import logging
from lamson.routing import route, route_like, stateless
from config.settings import relay, database
from lamson import view
Notice that we include elements from the @lamson.routing@ that are decorators we use to configure a route. Then you'll see that we're getting that @settings.relay@ and @settings.database@ we configured in the previous sections. Finally we bring in the @lamson.view@ module directory to make rendering templates into email messages a lot easier. Now take a look at the rest of the file and you'll how a handler is structured: # Each state is a separate function in CAPS. It doesn't have to be, it just looks better. # Above each state function is a "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html, "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html, or "stateless":http://lamsonproject.org/docs/api/lamson.routing-module.html#stateless decorator to configure how @lamson.routing.Router@ uses it. # The "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html decorator takes a pattern and then regex keyword arguments to fill it in. The words in the pattern string are replaced in the final more complex routing regex by the keyword arguments after. However, *if you want to use regex directly you can*, "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html just needs a string that eventually becomes a regex. # A state function changes state by returning the next function to call. You want to go to the RUNNING state, just @return RUNNING@. # If any state function throws an error it will go into the @ERROR@ state, so if you make a state handler named ERROR it will get called on the next event and can recover. # If you want to run a state on this event rather than wait to have it run on the next, then simple call it and return what it returns. So to have RUNNING go now, just do @return RUNNING(message, ...)@ and it will work. # If a state has the same regex as another state, just use "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html to say that. # If you have a "stateless":http://lamsonproject.org/docs/api/lamson.routing-module.html#stateless decorator after a "route":http://lamsonproject.org/docs/api/lamson.routing.route-class.html or "route_like":http://lamsonproject.org/docs/api/lamson.routing.route_like-class.html, then that handler will run for *all* addresses that match, not just if this handler is in that state. That is pretty much the entire complexity of how you write a handler. You setup routes, and return the next step in your conversation as the next function to run. The @lamson.routing.Router@ then takes each message it receives and runs it through a processing loop handing it to your states and handlers. h2. How States Are Run The best way to see how states are processed is to look at the "Router":http://lamsonproject.org/docs/api/lamson.routing.RoutingBase-class.html code that does it:
    def deliver(self, message):
        if self.RELOAD: self.reload()

        called_count = 0

        for functions, matchkw in self.match(message['to']):
            to_call = []
            in_state_found = False

            for func in functions:
                if lamson_setting(func, 'stateless'):
                    to_call.append(func)
                elif not in_state_found and self.in_state(func, message):
                    to_call.append(func)
                    in_state_found = True

            called_count += len(to_call)

            for func in to_call:
                if lamson_setting(func, 'nolocking'):
                    self.call_safely(func, message,  matchkw)
                else:
                    with self.call_lock:
                        self.call_safely(func, message, matchkw)

        if called_count == 0:
            if self.UNDELIVERABLE_QUEUE:
                LOG.debug("Message to %r from %r undeliverable, putting in undeliverable queue.",
                          message['to'], message['from'])
                self.UNDELIVERABLE_QUEUE.push(message)
            else:
                LOG.debug("Message to %r from %r didn't match any handlers.",
                          message['to'], message['from'])
What this does is take all the handlers you've loaded, and then finds which handlers have a state function that matches the current message. It then goes through each potential match, and determines which of all the matching state functions is "in that state". This means that, even though you have six state functions that answer to "(list_name)-(action)@(host)" only the one that matches the users current state (say PENDING) will be called next. As it goes through these functions it also loads up any that are marked "stateless" so they can be called as well. Finally, it just calls them in order. If the message results in no methods to call, then it will take the message and tell you this, or put it into an @UNDELIVERABLE_QUEUE@ for you to review it later. bq. Slight design criticism: Currently the order of these calls is fairly deterministic, but you can't rely on it. It's also not clear if *all* matching states should run, or just the first. It currently only runs the first match, but it might be better to run each match from each handler. Suggestions welcome on this. h2. Debugging Routes In the old way of doing routing you would edit a large table of "routes" in your @config/settings.py@ file and then that told Lamson how to run. The problem with this is it was too hard to maintain and too hard to indicate that different states needed a different route. The new setup is great because all your routing for each handler module is right there, and it's easy to see what will cause a particular state function to go off. What sucks about the new setup is that you can't find out what all the routes are doing *globally* in one place. That's where @lamson routes@ comes in. Simply run that command and you'll get a debug dump of all the full routing regex and the functions and modules they belong to:
Routing ORDER:  ['^(?P<address>.+)@(?P<host>test\\.com)$']
Routing TABLE: 
---
'^(?P<address>.+)@(?P<host>test\\.com)$':  app.handlers.sample.START  app.handlers.sample.NEW_USER
   app.handlers.sample.END  app.handlers.sample.FORWARD  
---
This is telling you which regex is matched first, then what those regex are mapped to. This is very handy as you can copy-paste that regex right into a python shell and then play with it to see if it would match what you want. You can also pass in an email address to the @-test@ option and it will tell you what routes would match and which functions that will call:
osb $ lamson routes -test test.blog@oneshotblog.com
2009-06-07 02:33:31,678 - root - INFO - Database configured to use sqlite:///app/data/main.db URL.
Routing ORDER:  [... lots of regex here ...]
Routing TABLE: 
---
... each regex and what state functions it maps ..
---
'^post-confirm-(?P[a-z0-9]+)@(?Poneshotblog\\.com)$':  app.handlers.post.CONFIRMING  
---

TEST address 'test.blog@oneshotblog.com' matches:
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.index.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.START
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
  '^(?P[a-zA-Z0-9][a-zA-Z0-9.]+)@(?Poneshotblog\\.com)$' app.handlers.post.POSTING
  -  {'host': 'oneshotblog.com', 'post_name': 'test.blog'}
osb $ 
If you're working with Lamson this is incredibly helpful, because it tells you what routes you have, what functions they call, and then it'll take an email address and tell you all the routes that match it. h2. THREADING! Lamson takes a lighter approach to how it runs. It assumes that most of the time you want lamson to keep itself sane with minimal locking, and that you want each of your state functions to run in a thread lock that prevents others from stepping on your operations. In 95% of the cases, this is what you want. To accomplish this, Lamson's router will acquire an internal lock for operations that change its state, and a separate lock before it calls each state function. Since multiple state functions run inside each thread, but one thread handles each message, you'll get multiple processing, but each state won't step on other states in the system. However, it's those 5% of the times that will kill your application, and if you know what you're doing, you should be able to turn this off. In order to tell the Router *not* to lock your state function, simply decorate it with "nolocking":http://lamsonproject.org/docs/api/lamson.routing-module.html#nolocking and Lamson will skip the locking and just run your state raw. This means that other threads will run potentially stepping on your execution, so you *must* do your own locking. Now, don't think that slapping a "nolocking":http://lamsonproject.org/docs/api/lamson.routing-module.html#nolocking on your state functions is some magic cure for performance issues. You only ever want to do this if you *really* know your stuff, and you know how to make that operation faster with better controlled locking. The reality is, if you have an operation that takes so long it blocks everything else, then you are doing it wrong by trying to do it all in your state function. You should change your design so that this handler drops the message into a "lamson.queue.Queue":http://lamsonproject.org/docs/api/lamson.queue.Queue-class.html and that *another* Lamson server reads messages out of that to do the long running processing. Using queues and separate Lamson servers you can solve most of your processing issues without a lot of thread juggling and process locking. In fact, since Lamson uses maildir queues by default you can even spread these processors out to multiple machines reading off a shared disk and everything will be just fine. But, since programmers will always want to just try turning off the locking, Lamson supports the @nolocking@ decorator. Use with care. h2. What's In A Unit Test Writing unit tests is way outside the scope of this document, but you should read up on using nosetests, testunit, and you should look at "lamson.testing":http://lamsonproject.org/docs/api/lamson.testing-module.html for a bunch of helper functions. Also look in the generated @tests@ directory to see some examples. h2. Spell Checking Your Email Templates Another big help is that Lamson has support for "PyEnchant":http://www.rfk.id.au/software/pyenchant/ so you can spell check your templates. You can use "lamson.testing.spelling":http://lamsonproject.org/docs/api/lamson.testing-module.html#spelling function in your unit tests. Installing PyEnchant is kind of a pain, but the trick is to get the dictionary you want and put it in your @~/.enchant/myspell@ directory. You'll also want to open the @config/testing.py@ file and uncomment the lines at the bottom that tell PyEnchant where to find the enchant so (dylib). PyEnchant is kind of hard to use, so if you have suggestions on a better Python spell checking lib for unit tests please let "me know.":/contact.html h2. Spam Filtering For Free Lamson comes with the "lamson.spam":http://lamsonproject.org/docs/api/lamson.spam-module.html module which supports "SpamBayes":http://spambayes.sourceforge.net/ spam filtering system. Read the document on "Filtering Spam With Lamson":/docs/filtering_spam.html to get a full set of instructions on using the spam filtering features. h2. Other Examples Next you'll want to sink your teeth in a bigger example. Go grab "the source distribution .tar.gz":/releases/ and extract it so you can get at the examples:
$ tar -xzvf lamson-VERSION.tar.gz
$ cd lamson-VERSION
$ cd examples/osb
You are now in the osb example that is running on "oneshotblog.com":http://oneshotblog.com/. Using what you've learned so far you can start reviewing the code and finding out how a working example operates. h2. Getting Help As you work through this documentation, send your questions "to me":/contact.html and I'll try to help you. You can also join the "lamson@librelist.com mailing list":mailto:lamson@librelist.com and get help from other Lamson users. lamson-1.0pre11/doc/lamsonproject.org/output/docs/hooking_into_django.html0000644000076500000240000001546711243575424026472 0ustar zedshawstaff LamsonProject: Hooking Lamson Into Django

Hooking Lamson Into Django

This is a short document because using Django ORM is very simple from Lamson. The trick is to fake out Django so that when you import the Django model into Lamson, the Django model knows where it’s living and will operate. We’ll go through an “integration” step by step.

Step 1: Make Your Django App Work

There’s no point in trying to import an ORM that doesn’t work. So get it working, maybe write some tests.

An important thing is that you should be able to run python manage.py shell and import your model without problems.

Step 2: Fake Out Django

You then have to put an environment variable in your Lamson config/settings.py file before you load any Django Models:

os.environ['DJANGO_SETTINGS_MODULE'] = 'webapp.settings'

This is from the librelist.com examples, where the Django models are in webapp so we our settings module from Lamson perspective is webapp.settings.

Step 3: Use The Django Models

After that, you can just import your models however you want. Here’s an example of Librelist using a Django Model to store confirmations:

from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()

Well, That Wasn’t Too Hard

The only thing you’ll have to contend with is where code you need to use these models lives. Since it’s all Python, you can just import what you need, but my recommendation is to focus most of your model code into your Django application, and then only put a small amount into Lamson.

For more information, look in the Lamson source releases to see how Django is used in the examples/librelist code.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/hooking_into_django.txt0000644000076500000240000000523511243575422026333 0ustar zedshawstaffTitle: Hooking Lamson Into Django This is a short document because using "Django":http://www.djangoproject.com/ ORM is very simple from Lamson. The trick is to fake out Django so that when you import the Django model into Lamson, the Django model knows where it's living and will operate. We'll go through an "integration" step by step. h1. Step 1: Make Your Django App Work There's no point in trying to import an ORM that doesn't work. So get it working, maybe write some tests. An important thing is that you should be able to run @python manage.py shell@ and import your model without problems. h2. Step 2: Fake Out Django You then have to put an environment variable in your Lamson @config/settings.py@ file before you load any Django Models:
os.environ['DJANGO_SETTINGS_MODULE'] = 'webapp.settings'
This is from the "librelist.com":http://librelist.com/ examples, where the Django models are in @webapp@ so we our settings module from Lamson perspective is @webapp.settings@. h2. Step 3: Use The Django Models After that, you can just import your models however you want. Here's an example of Librelist using a Django Model to store confirmations:
from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()
h2. Well, That Wasn't Too Hard The only thing you'll have to contend with is where code you need to use these models lives. Since it's all Python, you can just import what you need, but my recommendation is to focus most of your model code into your Django application, and then only put a small amount into Lamson. For more information, look in the "Lamson source releases":/releases/ to see how Django is used in the @examples/librelist@ code. lamson-1.0pre11/doc/lamsonproject.org/output/docs/html_email_generation.html0000644000076500000240000002615611243706471027002 0ustar zedshawstaff LamsonProject: HTML Email Generation

HTML Email Generation

HTML Email is apparently the killer feature for everyone who uses email. The first, second, and third question I get asked when I tell people about Lamson is, “Does it do HTML email?!” Yes it does, and hopefully in a very nice clean way that’ll make blasting out all that great marketing material your users love easier than ever.

The fourth question is whether Lamson handles bounces. Yes it does.

A Few Tips About HTML Email

First, you must educate your marketing people that your user’s Inbox is not a TV. I actually run myinboxisnota.tv just to make a point that people who do marketing have the wrong idea of people’s Inbox experience. It’s your job as the hacker to educate them and make sure that they know users actually mostly hate HTML email, and that sending them an HTML email is nothing like sending them a commercial on TV.

The next tip is a technical one related to how you have to craft your HTML. Lamson’s lamson.html API actually helps you get this right, but you need to understand it in order to appreciate what it does for you.

The rule when crafting your HTML is that you need to code for browsers of circa 1995 with no ability to use style tags or script tags. Most of the various HTML viewing clients strip or disable these making them useless. This means you have to put all CSS stylings inline into your HTML, and you have to rely on “taboo” tags like center and table for your layout.

The Structure Of HtmlMail

The lamson.html.HtmlMail class is responsible for generating all your HTML emails. How it works is you give construct one with an “outer template” and a CleverCSS template as your CSS stylesheet. This builds a generator that you then hand an internal version of your email to generate the content for each user.

This combination of outer template+CleverCSS and inner markdown content means that you can use the markdown content also as your text version, since markdown actually looks half-decent as a text format.

Here’s a simple example that shows this process in action:

generator = html.HtmlMail("style.css", "html_test.html", {})

resp = generator.respond({}, "content.markdown",
                       From="zedshaw@zedshaw.com",
                       To="zedshaw@zedshaw.com",
                       Subject="Test of an HTML mail.",
                       Body="content.markdown")

First, we make a generator passing in the CleverCSS stylesheet style.css, and the Jinja2 HTML template html_test.html. Then we call HtmlMail.respond to craft a lamson.mail.MailResponse that you can then hand to lamson.server.Relay for delivery.

There’s also a couple of tricks in the above email. First, notice that the second parameter is “content.markdown”, but that we also pass in the Body=“content.markdown”. This little convenience tells the HtmlMail object that you want to reuse the raw content.markdown file as the text/plain version of the email.

Finally, once you make the generator you can keep calling respond to spit out each message you want.

The HTML CSS Conversion

None of the above code shows you what is the nicest part of this API. The lamson.html API will actually take plain HTML and your CleverCSS and insert the style attributes into it for you. Let’s look at an example from the unit test (that’s disgusting). First we have the CleverCSS template:

body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h3:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black

Then we have the raw original HTML:

<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h3 id="dull">All done.</h3>
    </body>
</html>

Notice this is a template too, and that {{ content }} is your content.markdown file from the earlier discussion.

Now when you run this (including the content.markdown not shown here), Lamson produces this:

<html>
<head>
<title></title>
</head>
<body style="background: magenta; margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="font-size: 3em; background: black; color: white"></h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="font-size: 2em; color: yellow; color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h3 id="dull" style="font-size: 1em; background: gray; color: black">All done.</h3>
</body>
</html>

Which, if you code for a Web 2.0 company is probably making your eyes bleed Dijon mustard, but it works. Lamson has walked your HTML and inserted all the style tags it could, including keeping any you already had there.

Conclusion

With Lamson HTML email API you should be able to blast out all the wonderful HTML you need to prop up your sales needs for years to come. It does the best it can to make it easy to still work in a modern web methodology, but produce the nasty HTML that has the highest chance of working in most email clients.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/html_email_generation.txt0000644000076500000240000001413711243706470026650 0ustar zedshawstaffTitle: HTML Email Generation HTML Email is apparently the killer feature for everyone who uses email. The first, second, and third question I get asked when I tell people about Lamson is, "Does it do HTML email?!" Yes it does, and hopefully in a very nice clean way that'll make blasting out all that great marketing material your users *love* easier than ever. bq. The fourth question is whether Lamson handles bounces. "Yes it does.":/docs/bounce_detection.html h2. A Few Tips About HTML Email First, you *must* educate your marketing people that your user's "Inbox is not a TV.":http://myinboxisnota.tv/ I actually run "myinboxisnota.tv":http://myinboxisnota.tv/ just to make a point that people who do marketing have the wrong idea of people's Inbox experience. It's your job as the hacker to educate them and make sure that they know users actually mostly hate HTML email, and that sending them an HTML email is nothing like sending them a commercial on TV. The next tip is a technical one related to how you have to craft your HTML. Lamson's "lamson.html":http://lamsonproject.org/docs/api/lamson.html-module.html API actually helps you get this right, but you need to understand it in order to appreciate what it does for you. The rule when crafting your HTML is that you need to code for browsers of circa 1995 with no ability to use @style@ tags or @script@ tags. Most of the various HTML viewing clients strip or disable these making them useless. This means you have to put all CSS stylings inline into your HTML, and you have to rely on "taboo" tags like @center@ and @table@ for your layout. h2. The Structure Of HtmlMail The "lamson.html.HtmlMail":http://lamsonproject.org/docs/api/lamson.html.HtmlMail-class.html class is responsible for generating all your HTML emails. How it works is you give construct one with an "outer template" and a "CleverCSS":http://sandbox.pocoo.org/clevercss/ template as your CSS stylesheet. This builds a generator that you then hand an internal version of your email to generate the content for each user. This combination of outer template+CleverCSS and inner markdown content means that you can use the markdown content also as your text version, since markdown actually looks half-decent as a text format. Here's a simple example that shows this process in action:
generator = html.HtmlMail("style.css", "html_test.html", {})

resp = generator.respond({}, "content.markdown",
                       From="zedshaw@zedshaw.com",
                       To="zedshaw@zedshaw.com",
                       Subject="Test of an HTML mail.",
                       Body="content.markdown")
First, we make a generator passing in the CleverCSS stylesheet @style.css@, and the Jinja2 HTML template @html_test.html@. Then we call @HtmlMail.respond@ to craft a "lamson.mail.MailResponse":http://lamsonproject.org/docs/api/lamson.mail.MailResponse-class.html that you can then hand to "lamson.server.Relay":http://lamsonproject.org/docs/api/lamson.server.Relay-class.html for delivery. There's also a couple of tricks in the above email. First, notice that the second parameter is "content.markdown", but that we also pass in the Body="content.markdown". This little convenience tells the HtmlMail object that you want to reuse the raw @content.markdown@ file as the text/plain version of the email. Finally, once you make the generator you can keep calling @respond@ to spit out each message you want. h2. The HTML CSS Conversion None of the above code shows you what is the nicest part of this API. The lamson.html API will actually take plain HTML and your CleverCSS and insert the @style@ attributes into it for you. Let's look at an example from the unit test (that's disgusting). First we have the CleverCSS template:
body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h3:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black
Then we have the raw original HTML:
<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h3 id="dull">All done.</h3>
    </body>
</html>
Notice this is a template too, and that {{ content }} is your @content.markdown@ file from the earlier discussion. Now when you run this (including the content.markdown not shown here), Lamson produces this:
<html>
<head>
<title></title>
</head>
<body style="background: magenta; margin: 10; padding: 20; background: #006200; color: blue">
<h1 class="bright" style="font-size: 3em; background: black; color: white"></h1>
<h1 style="font-size: 3em">Hello</h1>
<p style="padding: 0.3em; background: red">I would <em>love</em> for you to tell me what is going on here joe.  NOW!</p>
<h2 style="font-size: 2em; color: yellow; color: yellow">Alright</h2>
<p style="padding: 0.3em; background: red">This is the best I can come up with.</p>
<p style="padding: 0.3em; background: red">Zed</p>
<h3 id="dull" style="font-size: 1em; background: gray; color: black">All done.</h3>
</body>
</html>
Which, if you code for a Web 2.0 company is probably making your eyes bleed Dijon mustard, but it works. Lamson has walked your HTML and inserted all the style tags it could, including keeping any you already had there. h2. Conclusion With Lamson HTML email API you should be able to blast out all the wonderful HTML you need to prop up your sales needs for years to come. It does the best it can to make it easy to still work in a modern web methodology, but produce the nasty HTML that has the highest chance of working in most email clients. lamson-1.0pre11/doc/lamsonproject.org/output/docs/index.html0000644000076500000240000002037511313464306023554 0ustar zedshawstaff LamsonProject: Lamson Project Documentation

Lamson Project Documentation

This is the Lamson documentation, organized into categories from “newbie” to “expert”. You should also check out the screen casts available which might help you if you’re the more mediated type.

The documentation is only updated after major releases, and it may not have the best information. If you are following the docs and they’re wrong, then send an email to lamson@librelist.com and report it.

Finally, every document is also uploaded with it’s text version.

Initial Concepts

Advanced Concepts

Deployment

  • Deploying Lamson Level 0 With Virtualenv And Pip — Quick instructions for setting up your Lamson in a virtualenv for your first simple deployment.
  • Deploying Lamson Level 1 — This is for when you’re getting more serious about deployment. Involves building a completely separate virtualenv+python for Lamson and shows deploying oneshotblog in it.
  • Deploying Lamson Level 2 — At this level of deployment you are running multiple sites on the same server using a virtualhost setup, and you have each application installed under its own user.

Deployment: Examples

Specific Features

  • Unit Testing — Lamson has a few simple things to help write better mail specific unit tests.
  • Confirmations — Confirming a user so that you validate they are an actual email address.
  • Filtering Spam — How to use Lamson’s spam blocking features. It’s easy to use, but a bit hairy to setup.
  • Bounce Detection — Using Lamson’s bounce message parser to handle bounces.
  • Unicode Encoding And Decoding — How Lamson decodes the nastiest email into Unicode, and then converts Unicode back into a clean email for sending.
  • HTML Email Generation — Using Lamson’s HTML email generation library to send out HTML to annoy everyone with.

lamson-1.0pre11/doc/lamsonproject.org/output/docs/index.txt0000644000076500000240000000627711243570044023433 0ustar zedshawstaffTitle: Lamson Project Documentation This is the Lamson documentation, organized into categories from "newbie" to "expert". You should also check out the "screen casts":/videos/ available which might help you if you're the more mediated type. The documentation is only updated after major releases, and it may not have the best information. If you are following the docs and they're wrong, then "send an email to lamson@librelist.com":mailto:lamson@librelist.com and report it. Finally, every document is also uploaded with it's text version. h2. Initial Concepts * "Frequently Asked Question":/docs/faq.html -- Questions people have asked about Lamson. * "Getting Started":/docs/getting_started.html -- A fast tour of getting Lamson going and doing something with it. * "Lamson Commands":/docs/lamson_commands.html -- All of the commands Lamson supports. You can get at this with @lamson -help@. * "Introduction To Finite State Machines":/docs/introduction_to_finite_state_machines.html -- Important to understand the simplified version of Finite State Machines Lamson uses. h2. Advanced Concepts * "Deferred Processing To Queues":/docs/deferred_processing_to_queues.html -- Very handy way of processing mail. * "Writing A Custom State Store":/docs/writing_a_state_storage.html -- You'll need this if you want to store state in the database. * "Primary Vs. Secondary Registration":/docs/primary_vs_secondary_registration.html -- The concept of doing registration in Lamson, where contacting the service the first time is the registration. * "Hooking Into Django":/docs/hooking_into_django.html -- Shows you how to get access to a Django ORM model. h2. Deployment * "Deploying Lamson Level 0 With Virtualenv And Pip":/docs/lamson_virtual_env.html -- Quick instructions for setting up your Lamson in a virtualenv for your first simple deployment. * "Deploying Lamson Level 1":/docs/deploying_lamson.html -- This is for when you're getting more serious about deployment. Involves building a completely separate virtualenv+python for Lamson and shows deploying oneshotblog in it. * "Deploying Lamson Level 2":/docs/deploying_lamson_level_2.html -- At this level of deployment you are running multiple sites on the same server using a virtualhost setup, and you have each application installed under its own user. h2. Deployment: Examples * "Deploying OneShotBlog":/docs/deploying_oneshotblog.html h2. Specific Features * "Unit Testing":/docs/unit_testing.html -- Lamson has a few simple things to help write better mail specific unit tests. * "Confirmations":/docs/confirmations.html -- Confirming a user so that you validate they are an actual email address. * "Filtering Spam":/docs/filtering_spam.html -- How to use Lamson's spam blocking features. It's easy to use, but a bit hairy to setup. * "Bounce Detection":/docs/bounce_detection.html -- Using Lamson's bounce message parser to handle bounces. * "Unicode Encoding And Decoding":/docs/unicode_encoding_and_decoding.html -- How Lamson decodes the nastiest email into Unicode, and then converts Unicode back into a clean email for sending. * "HTML Email Generation":/docs/html_email_generation.html -- Using Lamson's HTML email generation library to send out HTML to annoy everyone with. lamson-1.0pre11/doc/lamsonproject.org/output/docs/introduction_to_finite_state_machines.html0000644000076500000240000004226411242713720032274 0ustar zedshawstaff LamsonProject: A Painless Introduction To Finite State Machines

A Painless Introduction To Finite State Machines

Lamson uses the concept of a Finite State Machine to do the internal processing and keep track of what it should do next. You don’t necessarily need to know the details of how a FSM works, but it helps you if you want to know enough about Lamson to do advanced work. Most people could be blissfully unaware of state machines and still do plenty of work with Lamson.

Your Computer Science Class Sucked

When I say “finite state machine” everyone reads it as:

FINITE STATE MACHINE!!!!! OMGWTFBBQ THOSE ARE HARD!!!!”

Yes, the way your professor explained FSM makes them much harder than they are in practice. You were probably thrown fairly random sounding terms like “edge”, “node”, “transitions”, “acceptors”, “recognizers”, and “transducers”. You were probably shown graphs with lines and circles and told confusingly that the lines were edges and the circles were nodes, but that the edges were states unless you used this kind where the edges were transitions and that kind where the circles were states.

Alright, just forget about that because they aren’t really that complicated. A practical finite state machine is basically four things:

  • A bunch of functions, or things that need to get done.
  • A bunch of events, or reasons to call these functions.
  • Some piece of data that tracks the “state” this bunch of functions is in.
  • Code inside the functions that says how to “transition” or “change” into the next state for further processing.

That is really all there is to every FSM. Sure you can change around what a state is, add code that runs on a transition, or make FSM that “inherit” from other FSM, but in the end they are all:

functions, events, states, transitions

That is all there is to it, no matter how you slice these or dice these up.

Implementing A Practical FSM

Now that we’ve brought the fear level down, let’s actually make one. This will be a little toy FSM, but it will get you started and it won’t be too far off from what Lamson uses.

We are going to implement your classic email confirmation system. This system usually works like this:

  1. You receive an email at some service, let’s say users-subscribe@test.com.
  2. You reply to the sender with a confirmation message.
  3. They reply with an email to your confirmation’s random address.
  4. You receive this, approve the handshake, and make them subscribed with a welcome email.
  5. They send an email, and since they are subscribed, you post it to the list.

This is a typical operation on most mailing lists, but with a state machine it turns out to be fairly trivial to implement.

First, we need to look at the above conversation and think about what is more clear: states the user will be in, or events that they will generate. In this case I’d go with the states they will be in during the conversation:

  • START — Every machine has one of these.
  • PENDING — We sent the confirm, and waiting for their reply.
  • POSTING — They replied to the confirm and can now post.

Notice I gave each state a verb. You are saying what state this user is in. “They are currently posting.” You don’t say what they “did”. Now, sometimes it just makes sense to do it differently, so don’t be a slave to this setup. I’ve just found it helps keep things consistent if you name the states after what they are currently doing.

Now that we know what kind of states we’re dealing with we need to solve the next two parts of the puzzle: events or transitions. In an email application I like to start with the events, as these are the email addresses and messages they will be sending me. What we do now is for each state, list the addresses we’ll consider an “event” for that state.

  • START — (list_name)-subscribe@test.com
  • PENDING — (list_name)-confirm-(id)@test.com
  • POSTING — (list_name)@test.com

This is all the “events” we need right now, written out as email addresses.

If we were to add the ability to unsubscribe, then we would add an event for (list_name)-unsubscribe@test.com to POSTING so that we could transition them from POSTING to say, SLEEPING.

We now have our states, and the events that each state answers. Last step is to figure out what state “transitions” to which other state.

Write The Functions

At this point, our FSM is simple enough that we could just write the functions, and the logic of each function would dictate the transitions. However, if you had a larger number of states and events you would want to sit down and draw a diagram or a make a table of the transitions before you wrote some code.

To start our functions we’ll just name them after the states and put the events they handle at the top as pseudo code event decorators:

@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    ...

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    ...

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    ...

This is abbreviating the syntax quite a bit, is not functioning Python code but it is pretty close. Instead of event you would have a route decorator and it would have a few regexes. Otherwise it’s about the same.

We could also say that each state handles multiple events, which is what you would do if POSTING handled the “unsubscribe” requests.

Add Logic And Transitions

The final piece, and the part you’ll spend the longest getting right, is filling in the logic and making the transitions happen. How would you indicate where each state should go next? Remember that each state is a simple Python function, and that to “transition” means to change to another state. Well, we have to tell whatever is running the state machine to run a different function next time.

Easiest way to do that is for our handlers to just return the next function. The “runner” will then take that, store it somewhere, and the next time an event comes the runner will load the next function to run.

Here’s the psuedo code to do just that:

@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    send_confirmation()
    return PENDING

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    if check_confirmation_is_good():
        send_welcome()
        return POSTING
    else:
        ignore_them()

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    deliver_message_to_all()
    return POSTING

Right away you can see that we change to the next state by just returning the actual function to call next. When the next event comes in, the runner matches it to the right function, calls it, and then sets up for the next one.

If you look at PENDING, you can see that it either returns POSTING if they confirm correctly or it just ignores them. You could also transition to “return ERROR” if you wanted to put them in an error state and send a different message.

Looking at POSTING, you see that it just keeps returning itself to indicate that it is staying there. If you had POSTING process “unsubscribes” then it would simply do the unsubscribe confirm, and then transition to UNSUBSCRIBING. That state function would then check the confirmation and unsubscribe them, transitioning to something like DEAD or SLEEPING.

An “optimization” in Lamson is that if a state function doesn’t return anything then it’s assumed to just want to stay in that state. In this case POSTING could have no return and it would work the same.

Jump To vs. Transition

You now know pretty much everything you need to handle FSM except for a tiny corner case. It is typically called the “epsilon transition” which basically means “transition to that state without an explicit event”. When you use this is when your event needs to fire off the code for the next transition right now, rather than waiting for the next event from the user.

In the above pseudo code this is simply done by actually calling that transition function and returning whatever it returns. Let’s say we wanted to have PENDING also call POSTING with the original message they sent:

def PENDING(message):
    if check_confirmation_is_good():
        send_welcome()
        return POSTING (message):
    else:
        ignore_them()

def POSTING(message):
    deliver_message_to_all()
    return POSTING

Notice that we changed the return POSTING to a return POSTING(...) which returns the results of calling POSTING.

That’s it, you now know epsilon transitions. Man, that was tough.

The danger of this is if you don’t have your FSM carefully mapped out, then you could call a state that loops back to your state and you’re in an endless loop. Watch for that.

Why Finite State Machines Anyway

Finite State Machines like this are very powerful because they behave in ways that are consistent, easily debugged, and intelligent. Because the decision of how each step in the chain of events is controlled by a constrained set of states, events, and transitions, you can actually avoid many bugs you’d get in regular classes and objects.

For example, if you were to receive another subscribe message while there is a confirmation pending, then that event (email address) isn’t recognize and just ignored. You would see it in the logs, and see that your FSM stayed in the PENDING state. If you then wanted PENDING to handle additional requests it’s a simple matter of adding that event and writing the code.

The way to think of the a FSM is it is like an object that has a white list of functions, parameters, AND allowed values for its private data. If an FSM tries to change to a state that doesn’t exist it’s an error. If it gets an event it doesn’t know it ignores it. If you try to transition wrong you see it in the logs, or it’s an error.

FSM also have the debugging advantage of showing you the history of not only what states were called, but why they were called and what they did next. You will see the entire conversation and can pinpoint exactly where it went wrong.

However, the most important reason to use FSM is because this is how email and asynchronous conversations work. When you have a conversation with someone there is state involved. You don’t have to start the entire conversation from scratch at the start of each sentence. Instead you remember what the person said and what state you are in (angry, sad, happy) and that controls what you do next (punch them, run away, hug them) based on the events they send (“screw you”, “you’re dead”, “i love you”).

The use of an FSM will make your Lamson applications seem like magic. They will behave more like smart systems that just seems to know like a wizard what should happen next, and why it should happen that way.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/introduction_to_finite_state_machines.txt0000644000076500000240000002545111242713711032146 0ustar zedshawstaffTitle: A Painless Introduction To Finite State Machines Lamson uses the concept of a Finite State Machine to do the internal processing and keep track of what it should do next. You don't necessarily need to know the details of how a FSM works, but it helps you if you want to know enough about Lamson to do advanced work. Most people could be blissfully unaware of state machines and still do plenty of work with Lamson. h2. Your Computer Science Class Sucked When I say "finite state machine" everyone reads it as: bq. "FINITE STATE MACHINE!!!!! OMGWTFBBQ THOSE ARE HARD!!!!" Yes, the way your professor explained FSM makes them much harder than they are in practice. You were probably thrown fairly random sounding terms like "edge", "node", "transitions", "acceptors", "recognizers", and "transducers". You were probably shown graphs with lines and circles and told confusingly that the lines were edges and the circles were nodes, but that the edges were states unless you used this kind where the edges were transitions and that kind where the circles were states. Alright, just forget about that because they aren't really that complicated. A practical finite state machine is basically four things: * A bunch of functions, or things that need to get done. * A bunch of events, or reasons to call these functions. * Some piece of data that tracks the "state" this bunch of functions is in. * Code inside the functions that says how to "transition" or "change" into the next state for further processing. That is really all there is to every FSM. Sure you can change around what a state is, add code that runs on a transition, or make FSM that "inherit" from other FSM, but in the end they are all: bq. functions, events, states, transitions That is all there is to it, no matter how you slice these or dice these up. h2. Implementing A Practical FSM Now that we've brought the fear level down, let's actually make one. This will be a little toy FSM, but it will get you started and it won't be too far off from what Lamson uses. We are going to implement your classic email confirmation system. This system usually works like this: # You receive an email at some service, let's say users-subscribe@test.com. # You reply to the sender with a confirmation message. # They reply with an email to your confirmation's random address. # You receive this, approve the handshake, and make them subscribed with a welcome email. # They send an email, and since they are subscribed, you post it to the list. This is a typical operation on most mailing lists, but with a state machine it turns out to be fairly trivial to implement. First, we need to look at the above conversation and think about what is more clear: states the user will be in, or events that they will generate. In this case I'd go with the states they will be in during the conversation: * START -- Every machine has one of these. * PENDING -- We sent the confirm, and waiting for their reply. * POSTING -- They replied to the confirm and can now post. Notice I gave each state a verb. You are saying what state this user is in. "They are currently posting." You don't say what they "did". Now, sometimes it just makes sense to do it differently, so don't be a slave to this setup. I've just found it helps keep things consistent if you name the states after what they are currently doing. Now that we know what kind of states we're dealing with we need to solve the next two parts of the puzzle: events or transitions. In an email application I like to start with the events, as these are the email addresses and messages they will be sending me. What we do now is for each state, list the addresses we'll consider an "event" for that state. * START -- (list_name)-subscribe@test.com * PENDING -- (list_name)-confirm-(id)@test.com * POSTING -- (list_name)@test.com This is all the "events" we need right now, written out as email addresses. bq. If we were to add the ability to unsubscribe, then we would add an event for (list_name)-unsubscribe@test.com to POSTING so that we could transition them from POSTING to say, SLEEPING. We now have our states, and the events that each state answers. Last step is to figure out what state "transitions" to which other state. h2. Write The Functions At this point, our FSM is simple enough that we could just write the functions, and the logic of each function would dictate the transitions. However, if you had a larger number of states and events you would want to sit down and draw a diagram or a make a table of the transitions before you wrote some code. To start our functions we'll just name them after the states and put the events they handle at the top as pseudo code @event@ decorators:
@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    ...

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    ...

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    ...
This is abbreviating the syntax quite a bit, is not functioning Python code but it is pretty close. Instead of @event@ you would have a @route@ decorator and it would have a few regexes. Otherwise it's about the same. bq. We could also say that each state handles multiple events, which is what you would do if POSTING handled the "unsubscribe" requests. h2. Add Logic And Transitions The final piece, and the part you'll spend the longest getting right, is filling in the logic and making the transitions happen. How would you indicate *where* each state should go next? Remember that each state is a simple Python function, and that to "transition" means to change to another state. Well, we have to tell whatever is running the state machine to run a *different* function next time. Easiest way to do that is for our handlers to just return the next function. The "runner" will then take that, store it somewhere, and the next time an event comes the runner will load the next function to run. Here's the psuedo code to do just that:
@event("(list_name)-subscribe@test.com")
def START(...):
    """Initial setup of the user."""
    send_confirmation()
    return PENDING

@event("(list_name)-confirm-(id)@test.com")
def PENDING(...):
    """Waiting for them to confirm."""
    if check_confirmation_is_good():
        send_welcome()
        return POSTING
    else:
        ignore_them()

@event("(list_name)@test.com")
def POSTING(...):
    """They are posting, anything they send we post."""
    deliver_message_to_all()
    return POSTING
Right away you can see that we change to the next state by just returning the actual function to call next. When the next event comes in, the runner matches it to the right function, calls it, and then sets up for the next one. If you look at PENDING, you can see that it either returns POSTING if they confirm correctly or it just ignores them. You could also transition to "return ERROR" if you wanted to put them in an error state and send a different message. Looking at POSTING, you see that it just keeps returning itself to indicate that it is staying there. If you had POSTING process "unsubscribes" then it would simply do the unsubscribe confirm, and then transition to UNSUBSCRIBING. That state function would then check the confirmation and unsubscribe them, transitioning to something like DEAD or SLEEPING. bq. An "optimization" in Lamson is that if a state function doesn't return anything then it's assumed to just want to stay in that state. In this case POSTING could have no return and it would work the same. h2. Jump To vs. Transition You now know pretty much everything you need to handle FSM except for a tiny corner case. It is typically called the "epsilon transition" which basically means "transition to that state without an explicit event". When you use this is when your event needs to fire off the code for the *next* transition right now, rather than waiting for the next event from the user. In the above pseudo code this is simply done by actually calling that transition function and returning whatever it returns. Let's say we wanted to have PENDING also call POSTING with the original message they sent:
def PENDING(message):
    if check_confirmation_is_good():
        send_welcome()
        return POSTING (message):
    else:
        ignore_them()

def POSTING(message):
    deliver_message_to_all()
    return POSTING
Notice that we changed the @return POSTING@ to a @return POSTING(...)@ which returns the results of calling POSTING. That's it, you now know epsilon transitions. Man, that was tough. bq. The danger of this is if you don't have your FSM carefully mapped out, then you could call a state that loops back to your state and you're in an endless loop. Watch for that. h2. Why Finite State Machines Anyway Finite State Machines like this are very powerful because they behave in ways that are consistent, easily debugged, and intelligent. Because the decision of how each step in the chain of events is controlled by a constrained set of states, events, and transitions, you can actually avoid many bugs you'd get in regular classes and objects. For example, if you were to receive another subscribe message while there is a confirmation pending, then that event (email address) isn't recognize and just ignored. You would see it in the logs, and see that your FSM stayed in the PENDING state. If you then wanted PENDING to handle additional requests it's a simple matter of adding that event and writing the code. The way to think of the a FSM is it is like an object that has a white list of functions, parameters, AND allowed values for its private data. If an FSM tries to change to a state that doesn't exist it's an error. If it gets an event it doesn't know it ignores it. If you try to transition wrong you see it in the logs, or it's an error. FSM also have the debugging advantage of showing you the history of not only what states were called, but *why* they were called and what they did next. You will see the entire conversation and can pinpoint exactly where it went wrong. However, the most important reason to use FSM is because this is how email and asynchronous conversations work. When you have a conversation with someone there is state involved. You don't have to start the entire conversation from scratch at the start of each sentence. Instead you remember what the person said and what state you are in (angry, sad, happy) and that controls what you do next (punch them, run away, hug them) based on the events they send ("screw you", "you're dead", "i love you"). The use of an FSM will make your Lamson applications seem like magic. They will behave more like smart systems that just seems to know like a wizard what should happen next, and *why* it should happen that way. lamson-1.0pre11/doc/lamsonproject.org/output/docs/lamson_commands.html0000644000076500000240000002163711242706607025625 0ustar zedshawstaff LamsonProject: Available Lamson Commands

Available Lamson Commands

The following is also available by running lamson help and you can get the help for each individual command with lamson help -for COMMAND replacing COMMAND with one of these listed below.

The format for the printed options show default options as an actual setting, and required options as a CAPITALIZED setting you must give. For example, in the send command:

lamson send -port 8825 -host 127.0.0.1 -debug 1 -sender EMAIL -to EMAIL -subject STR -body STR -attach False

The options -port, -host, -debug, and -file have default settings, but -sender, -to, -subject and -body require a STRing or EMAIL. Notice also that -file defaults to False which you can change by just including -file (that toggles it true).

lamson blast

Given a maildir, this command will go through each email
and blast it at your server.  It does nothing to the message, so
it will be real messages hitting your server, not cleansed ones.

lamson cleanse

Uses Lamson mail cleansing and canonicalization system to take an
input maildir (or mbox) and replicate the email over into another
maildir.  It's used mostly for testing and cleaning.

lamson gen

Generates various useful things for you to get you started.

lamson gen -project STR -FORCE False

lamson help

Prints out help for the commands. 

lamson help

You can get help for one command with:

lamson help -for STR

lamson log

Runs a logging only server on the given hosts and port.  It logs
each message it receives and also stores it to the run/queue 
so that you can make sure it was received in testing.

lamson log -port 8825 -host 127.0.0.1 \
        -pid ./run/log.pid -chroot False  \
        -chdir "." -umask False -uid False -gid False \
        -FORCE False

If you specify a uid/gid then this means you want to first change to
root, set everything up, and then drop to that UID/GID combination.
This is typically so you can bind to port 25 and then become "safe"
to continue operating as a non-root user.

If you give one or the other, this it will just change to that
uid or gid without doing the priv drop operation.

lamson queue

Let's you do most of the operations available to a queue.

lamson queue (-pop | -get | -remove | -count | -clear | -keys) -name run/queue

lamson restart

Simply attempts a stop and then a start command.  All options for both
apply to restart.  See stop and start for options available.

lamson routes

Prints out valuable information about an application's routing configuration
after everything is loaded and ready to go.  Helps debug problems with
messages not getting to your handlers.  Path has the search paths you want
separated by a ':' character, and it's added to the sys.path.

lamson routes -path $PWD -- config.testing -test ""

It defaults to running your config.testing to load the routes. 
If you want it to run the config.boot then give that instead:

lamson routes -- config.boot

You can also test a potential target by doing -test EMAIL.

lamson send

Sends an email to someone as a test message.
See the sendmail command for a sendmail replacement.

lamson send -port 8825 -host 127.0.0.1 -debug 1 \
        -sender EMAIL -to EMAIL -subject STR -body STR -attach False'

lamson sendmail

Used as a testing sendmail replacement for use in programs
like mutt as an MTA.  It reads the email to send on the stdin
and then delivers it based on the port and host settings.

lamson sendmail -port 8825 -host 127.0.0.1 -debug 0 -- [recipients]

lamson start

Runs a lamson server out of the current directory:

lamson start -pid ./run/smtp.pid -FORCE False -chroot False -chdir "." \
        -umask False -uid False -gid False -boot config.boot

lamson status

Prints out status information about lamson useful for finding out if it's
running and where.

lamson status -pid ./run/smtp.pid

lamson stop

Stops a running lamson server.  Give -KILL True to have it
stopped violently.  The PID file is removed after the 
signal is sent.  Give -ALL the name of a run directory and
it will stop all pid files it finds there.

lamson stop -pid ./run/smtp.pid -KILL False -ALL False

lamson version

    Prints the version of Lamson, the reporitory revision, and the
    file it came from.

lamson web

Starts a very simple files only web server for easy testing of applications
that need to make some HTML files as the result of their operation.
If you need more than this then use a real web server.

lamson web -basedir "." -port 8888 -host '127.0.0.1'

This command doesn't exit so you can view the logs it prints out.

lamson-1.0pre11/doc/lamsonproject.org/output/docs/lamson_commands.txt0000644000076500000240000001206411242706605025470 0ustar zedshawstaffTitle: Available Lamson Commands Content-Type: text/html

The following is also available by running lamson help and you can get the help for each individual command with lamson help -for COMMAND replacing COMMAND with one of these listed below.

The format for the printed options show default options as an actual setting, and required options as a CAPITALIZED setting you must give. For example, in the send command:

lamson send -port 8825 -host 127.0.0.1 -debug 1 -sender EMAIL -to EMAIL -subject STR -body STR -attach False

The options -port, -host, -debug, and -file have default settings, but -sender, -to, -subject and -body require a STRing or EMAIL. Notice also that -file defaults to False which you can change by just including -file (that toggles it true).

lamson blast

Given a maildir, this command will go through each email
and blast it at your server.  It does nothing to the message, so
it will be real messages hitting your server, not cleansed ones.

lamson cleanse

Uses Lamson mail cleansing and canonicalization system to take an
input maildir (or mbox) and replicate the email over into another
maildir.  It's used mostly for testing and cleaning.

lamson gen

Generates various useful things for you to get you started.

lamson gen -project STR -FORCE False

lamson help

Prints out help for the commands. 

lamson help

You can get help for one command with:

lamson help -for STR

lamson log

Runs a logging only server on the given hosts and port.  It logs
each message it receives and also stores it to the run/queue 
so that you can make sure it was received in testing.

lamson log -port 8825 -host 127.0.0.1 \
        -pid ./run/log.pid -chroot False  \
        -chdir "." -umask False -uid False -gid False \
        -FORCE False

If you specify a uid/gid then this means you want to first change to
root, set everything up, and then drop to that UID/GID combination.
This is typically so you can bind to port 25 and then become "safe"
to continue operating as a non-root user.

If you give one or the other, this it will just change to that
uid or gid without doing the priv drop operation.

lamson queue

Let's you do most of the operations available to a queue.

lamson queue (-pop | -get | -remove | -count | -clear | -keys) -name run/queue

lamson restart

Simply attempts a stop and then a start command.  All options for both
apply to restart.  See stop and start for options available.

lamson routes

Prints out valuable information about an application's routing configuration
after everything is loaded and ready to go.  Helps debug problems with
messages not getting to your handlers.  Path has the search paths you want
separated by a ':' character, and it's added to the sys.path.

lamson routes -path $PWD -- config.testing -test ""

It defaults to running your config.testing to load the routes. 
If you want it to run the config.boot then give that instead:

lamson routes -- config.boot

You can also test a potential target by doing -test EMAIL.

lamson send

Sends an email to someone as a test message.
See the sendmail command for a sendmail replacement.

lamson send -port 8825 -host 127.0.0.1 -debug 1 \
        -sender EMAIL -to EMAIL -subject STR -body STR -attach False'

lamson sendmail

Used as a testing sendmail replacement for use in programs
like mutt as an MTA.  It reads the email to send on the stdin
and then delivers it based on the port and host settings.

lamson sendmail -port 8825 -host 127.0.0.1 -debug 0 -- [recipients]

lamson start

Runs a lamson server out of the current directory:

lamson start -pid ./run/smtp.pid -FORCE False -chroot False -chdir "." \
        -umask False -uid False -gid False -boot config.boot

lamson status

Prints out status information about lamson useful for finding out if it's
running and where.

lamson status -pid ./run/smtp.pid

lamson stop

Stops a running lamson server.  Give -KILL True to have it
stopped violently.  The PID file is removed after the 
signal is sent.  Give -ALL the name of a run directory and
it will stop all pid files it finds there.

lamson stop -pid ./run/smtp.pid -KILL False -ALL False

lamson version

    Prints the version of Lamson, the reporitory revision, and the
    file it came from.

lamson web

Starts a very simple files only web server for easy testing of applications
that need to make some HTML files as the result of their operation.
If you need more than this then use a real web server.

lamson web -basedir "." -port 8888 -host '127.0.0.1'

This command doesn't exit so you can view the logs it prints out.
lamson-1.0pre11/doc/lamsonproject.org/output/docs/lamson_virtual_env.html0000644000076500000240000001327111242706246026354 0ustar zedshawstaff LamsonProject: Installing Lamson Into Virtualenv With Pip

Installing Lamson Into Virtualenv With Pip

The best way to install Lamson in a virtualenv is using the pip tool to install it. This will automatically fetch the code from bazaar, install it inside the virtualenv and fetch any missing dependencies.

First, if you haven’t already, you’ll need to install pip. Make sure you’ve activated the virtualenv:

. bin/activate

from the root of the virtualenv, and then run:

easy_install -U pip

If you have bazaar installed on your machine, then just do:

pip install -e bzr+http://bazaar.launchpad.net/~zedshaw/lamson/development/#egg lamson

This’ll install the development version of lamson into a src/lamson directory (src/ will be created if it doesn’t exist). It will also install all of Lamson’s dependencies into the virtualenv. If you want to update to the latest development version of Lamson, run:

cd src/lamson
bzr pull

This will fetch the latest bazaar tip from Launchpad.

If you don’t have bazaar installed and don’t want to install it, you can grab the latest release of Lamson like this:

pip install -F http://launchpad.net/lamson/trunk/0.9-pre2/+download/lamson-0.9-pre2.tar.gz

Remember though that if you do install it like this you’ll need to manually update it by visiting Launchpad, finding the latest tarball URL and `pip install`ing it.

Credits

These instructions were donated to Lamson by Zachary Voase but contact me for suggested improvements.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/lamson_virtual_env.txt0000600000076500000240000000315511242705742026217 0ustar zedshawstaffTitle: Installing Lamson Into Virtualenv With Pip The best way to install Lamson in a virtualenv is using the "pip tool":http://pip.openplans.org/ to install it. This will automatically fetch the code from bazaar, install it inside the virtualenv and fetch any missing dependencies. First, if you haven't already, you'll need to install pip. Make sure you've activated the virtualenv:
. bin/activate
from the root of the virtualenv, and then run:
easy_install -U pip
If you have bazaar installed on your machine, then just do:
pip install -e bzr+http://bazaar.launchpad.net/~zedshaw/lamson/development/#egg lamson
This'll install the development version of lamson into a src/lamson directory (src/ will be created if it doesn't exist). It will also install all of Lamson's dependencies into the virtualenv. If you want to update to the latest development version of Lamson, run:
cd src/lamson
bzr pull
This will fetch the latest bazaar tip from Launchpad. If you don't have bazaar installed and don't want to install it, you can grab the latest release of Lamson like this:
pip install -F http://launchpad.net/lamson/trunk/0.9-pre2/+download/lamson-0.9-pre2.tar.gz
Remember though that if you do install it like this you'll need to manually update it by visiting Launchpad, finding the latest tarball URL and `pip install`ing it. h2. Credits These instructions were donated to Lamson by "Zachary Voase":http://disturbyte.github.com but "contact me":/contact.html for suggested improvements. lamson-1.0pre11/doc/lamsonproject.org/output/docs/primary_vs_secondary_registration.html0000644000076500000240000002700411242714125031473 0ustar zedshawstaff LamsonProject: Primary vs. Secondary Registration

Primary vs. Secondary Registration

When you design a Lamson application the question of “registration” comes up almost immediately. The question is one of how you find out who a person is so that you can interact with them. In the web world this involves a form of some kind asking for a login and password at a minimum. Some sites even go so far as to require a sign-up process before they let you see anything about the site, or even their marketing material.

On the web this kind of registration is what I like to call “Primary Registration”. It comes from the concept of primary vs. secondary data, or primary vs. secondary analysis. In the world of measurement a primary source is one that you gathered the information from directly, usually by giving them a questionaire. A secondary source is one that you gathered in some indirect way, either by evaluating past research in a new way, data mining, or simply unobtrusive data collection.

With a web site you must have a primary registration system. There is no implied identity in a browser that is consistent for a user, so you need to ask them for some information and associate their stateless browser to a cookie or similar tracking mechanism. You have to do this for each session you start with them, and to register them if your service requires some kind of password protection.

In the world of the web primary registration is just how things are done.

Secondary Registration

Lamson give you the ability to do Secondary Registration where the act of interacting with your service is the registration. A typical primary registrationg process will ask for a user’s email address to identify the user, but in Lamson the user actively gives your service their email address to start playing. Their first message to your service actually has all the information you need to sign them up and give them their first taste of your service.

In fact, a user’s identity is so baked into the email protocols that your Lamson server has almost the inverse problem: it’s too easy to fake an identity, so you need to confirm their subscription and important actions. This confirmation is traditionally nothing more than a reply asking them to reply with some random number in the address. If you get a reply to this auto-generated email address then you assume they are real.

This “random number confirmation handshake” is important because SMTP allows anyone with an internet connection to lie about who they are and craft a fake email claiming to be someone else. The confirmation assures you that the supposed sender has at least received an email in their inbox and that it had the random number you generated in the reply address. A determined hacker could also get past that, but if you have that situation you are dealing with more problems than just confirmations.

Once you do though you are fairly assured that they are identified and using your service. It ends up being the most low effort entry to your service you could possibly devise without making the service unsafe.

Comparing Two Designs

When you sit down to build a Lamson application you may still think about registration in the primary way, but a secondary registration is typically more natural for an email service. To illustrate the point, let’s say you are creating an online game using Lamson, and you want people to register for the game.

With a primary registration you would approach the problem by making your game open only to registered users. If they send an email to your game you send a single reply pointing them at the web page they can use to sign up. You also then take every email and pass it through a filter that checks that the user is registered in your subscription database. This is mostly how you would do it in a web application.

Maybe there is a legitimate reason to force a user to a web page to give a password and other information before using the service. If that’s the case, then go for it, but consider trying to make the entry to your service a secondary registration that then asks them to do a full registration to get more features.

With a secondary registration system, you are using Lamson’s ability to track the state of every user to know whether they are registered or not. When your game receives their first email, it would transition to an UNKNOWN state and reply with a confirmation email. Once they reply to the confirm email you transition from UNKNOWN to the rest of the game since they are now registered. Every interaction after that will simply know they are registered because they are in the right state.

This has a big impact on your game’s design because now you do not need to check the database on every received email to confirm their identity. Their email address and their state in your state’s storage tells you that implicitly.

You can also extend this to other requirements for your system. You can use their known state to transition them from free to paid quality services, to suspended states, to pause them after a bounce, and back to normal activity.

Invite Only Systems

An invite only system can also use the secondary registration technique by having the inviter’s invitation request create a record for the invitee that’s in the right start state. When the inviter tells you to invite their friend, you create a state for the friend that says INVITED and use the invite as a sort of implied confirmation email. When the invitee replies to this invite email, you transition them to the rest of the system as they’ve now registered.

Another way to think of an invite only system in Lamson is as a 3rd-party confirmation. Rather than the target user (the invitee) prompting a confirmation, it is their friend sending the invite (the inviter) who triggers the confirmation process on behalf of their buddy.

An additional bonus with Lamson is that the state of the invitee is known, so if the inviter tries to send them again, or if someone else does, then you know they already were sent an invite. You can just ignore the request and avoid spamming people. You can also run reports to find out your conversion rates by simply looking for everyone in the various states and counting.

Security And Design Issues

Doing these kind of secondary registrations and implied identity does come with an extra security penalty. Unless your system is incorporating S-MIME or GPG you won’t have a solid way to identify the sender. You don’t have this on most other internet services either so it’s not much of a loss. At least with email you have a higher assurance that this is a real person.

When you design your service it’s a good idea to:

  1. Either make everything the user does non-destructive,
  2. Send confirmations for anything desctructive,
  3. Or bounce them to a web site for more complex interactions and security.

You should also make any confirmations you send easily replied to, but have a good random number in them that you remember for later. In fact, if you use the “lamson.confirm” API you get all of this for free and done the right way.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/primary_vs_secondary_registration.txt0000644000076500000240000001575611242714123031357 0ustar zedshawstaffTitle: Primary vs. Secondary Registration When you design a Lamson application the question of "registration" comes up almost immediately. The question is one of how you find out who a person is so that you can interact with them. In the web world this involves a form of some kind asking for a login and password at a minimum. Some sites even go so far as to require a sign-up process *before* they let you see anything about the site, or even their marketing material. On the web this kind of registration is what I like to call "Primary Registration". It comes from the concept of primary vs. secondary data, or primary vs. secondary analysis. In the world of measurement a primary source is one that you gathered the information from directly, usually by giving them a questionaire. A secondary source is one that you gathered in some indirect way, either by evaluating past research in a new way, data mining, or simply unobtrusive data collection. With a web site you *must* have a primary registration system. There is no implied identity in a browser that is consistent for a user, so you need to ask them for some information and associate their stateless browser to a cookie or similar tracking mechanism. You have to do this for each session you start with them, and to register them if your service requires some kind of password protection. In the world of the web primary registration is just how things are done. h2. Secondary Registration Lamson give you the ability to do *Secondary Registration* where the act of interacting with your service *is* the registration. A typical primary registrationg process will ask for a user's email address to identify the user, but in Lamson the user actively gives your service their email address to start playing. Their first message to your service actually has all the information you need to sign them up and give them their first taste of your service. In fact, a user's identity is so baked into the email protocols that your Lamson server has almost the inverse problem: it's too easy to fake an identity, so you need to confirm their subscription and important actions. This confirmation is traditionally nothing more than a reply asking them to reply with some random number in the address. If you get a reply to this auto-generated email address then you assume they are real. bq. This "random number confirmation handshake" is important because SMTP allows anyone with an internet connection to lie about who they are and craft a fake email claiming to be someone else. The confirmation assures you that the supposed sender has at least received an email in their inbox and that it had the random number you generated in the reply address. A determined hacker could also get past that, but if you have that situation you are dealing with more problems than just confirmations. Once you do though you are fairly assured that they are identified *and* using your service. It ends up being the most low effort entry to your service you could possibly devise without making the service unsafe. h2. Comparing Two Designs When you sit down to build a Lamson application you may still think about registration in the primary way, but a secondary registration is typically more natural for an email service. To illustrate the point, let's say you are creating an online game using Lamson, and you want people to register for the game. With a primary registration you would approach the problem by making your game open only to registered users. If they send an email to your game you send a single reply pointing them at the web page they can use to sign up. You also then take every email and pass it through a filter that checks that the user is registered in your subscription database. This is mostly how you would do it in a web application. bq. Maybe there is a legitimate reason to force a user to a web page to give a password and other information before using the service. If that's the case, then go for it, but consider trying to make the *entry* to your service a secondary registration that then asks them to do a full registration to get more features. With a secondary registration system, you are using Lamson's ability to track the state of every user to know whether they are registered or not. When your game receives their first email, it would transition to an @UNKNOWN@ state and reply with a confirmation email. Once they reply to the confirm email you transition from @UNKNOWN@ to the rest of the game since they are now registered. Every interaction after that will simply know they are registered because they are in the right state. This has a big impact on your game's design because now you do *not* need to check the database on every received email to confirm their identity. Their email address and their state in your state's storage tells you that implicitly. You can also extend this to other requirements for your system. You can use their known state to transition them from free to paid quality services, to suspended states, to pause them after a bounce, and back to normal activity. h2. Invite Only Systems An invite only system can also use the secondary registration technique by having the inviter's invitation request create a record for the invitee that's in the right start state. When the inviter tells you to invite their friend, you create a state for the friend that says @INVITED@ and use the invite as a sort of implied confirmation email. When the invitee replies to this invite email, you transition them to the rest of the system as they've now registered. Another way to think of an invite only system in Lamson is as a 3rd-party confirmation. Rather than the target user (the invitee) prompting a confirmation, it is their friend sending the invite (the inviter) who triggers the confirmation process on behalf of their buddy. An additional bonus with Lamson is that the state of the invitee is known, so if the inviter tries to send them again, or if someone else does, then you know they already were sent an invite. You can just ignore the request and avoid spamming people. You can also run reports to find out your conversion rates by simply looking for everyone in the various states and counting. h2. Security And Design Issues Doing these kind of secondary registrations and implied identity does come with an extra security penalty. Unless your system is incorporating S-MIME or GPG you won't have a solid way to identify the sender. You don't have this on most other internet services either so it's not much of a loss. At least with email you have a higher assurance that this is a real person. When you design your service it's a good idea to: # Either make everything the user does non-destructive, # Send confirmations for anything desctructive, # Or bounce them to a web site for more complex interactions and security. You should also make any confirmations you send easily replied to, but have a good random number in them that you remember for later. In fact, if you use the "lamson.confirm" API you get all of this for free and done the right way. lamson-1.0pre11/doc/lamsonproject.org/output/docs/template.html0000644000076500000240000000013611224460100024236 0ustar zedshawstaff

$title

$content

Read more...

lamson-1.0pre11/doc/lamsonproject.org/output/docs/unicode_encoding_and_decoding.html0000644000076500000240000002631511243702542030416 0ustar zedshawstaff LamsonProject: Unicode Encoding And Decoding

Unicode Encoding And Decoding

The world is Unicode, but email existed long before that world. Lamson uses special encoding/decoding gear that takes just about any nasty horrible pre-Unicode email it gets and converts it to a perfect little Unicode fantasy. You do all your work in the Unicode world, which Python favors, and then when you’re ready to send, Lamson intelligently figures out how to make an email that anyone can read.

The added advantage of doing this conversion is that Lamson by default also cleans up all the various tricks spammers use to get around spam checkers. Because it’s doing a conversion from just first principles of “everything must become Unicode” the end result is a crystal clear piece of data you can filter. When you then output the same message, it gets reconverted to sane easily parseable message.

Lamson is also so persistent in this conversion that it can convert nearly every message that’s valid, and the very tiny percentage it can’t (less 1/1000th of a percent) are entirely screwed up spam that should not be transmitted anyway.

This gear apparently doesn’t support pre-Hindic sanskrit, but then again neither does Python.

Why

Sometimes people ask why Lamson would bother converting everything to Unicode? The reason is simple: everything you deal with now is Unicode. Either it’s in a representation like UTF-8, or it’s internally a Python Unicode string. Databases, web frameworks, ORM, network protocols, and the entire internet is Unicode.

However, there’s another practical reason. If you were to start using Lamson to process email, you would end up inventing almost the exact same gear to convert headers and bodies to Unicode. The only difference is you would do it in a typical hacker half-assed fashion that only solved your immediate problems, and you wouldn’t do it in a global useful way.

The Lamson encoding gear is basically what you should be making to deal with email in a modern language.

How

The lamson.encoding module is the main meat of this conversion magic. This code is probably the most dense part of Lamson since it has to use various parsing tricks and heuristics to figure out how to get every part of the message into a Unicode string.

The primary trick used is that Lamson does not trust anything it’s given, and when python fails to convert a header or body, it uses the wonderful chardet library to guess at what the encoding should be based on the contents. If this fails then the entire data is considered suspect and the message is rejected.

This turns out to be surprisingly accurate in practice, and it even corrects some invalid clients that fail to encode Subject lines but still place Chinese encodings in them. Lamson will take those unencoded headers, fail to convert them to ascii, run chardet on them to see they are actually Chinese, and then convert them accurately.

The Rules

A set of axioms controls how the encoding is done:

  1. NO ENCODING IS TRUSTED, NO LANGUAGE IS SACRED, ALL ARE SUSPECT.
  2. Python wants Unicode, it will get Unicode.
  3. Any email that CANNOT become Unicode, CANNOT be processed by Lamson or Python.
  4. Email addresses are ESSENTIAL to Lamson’s routing and security, and therefore will be canonicalized and properly encoded.
  5. Lamson will therefore try to “upgrade” all email it receives to Unicode internally, and cleaning all email addresses.
  6. It does this by decoding all codecs, and if the codec LIES, then it will attempt to statistically detect the codec using chardet.
  7. If it can’t detect the codec, and the codec lies, then the email is bad.
  8. All text bodies and attachments are then converted to Python Unicode in the same way as the headers.
  9. All other attachments are converted to raw strings as-is.

Once Lamson has done this, your Python handler can now assume that all MailRequest objects are happily Unicode enabled and ready to go. The rule is:

IF IT CANNOT BE UNICODE, THEN PYTHON CANNOT WORK WITH IT.

On the outgoing end (when you send a MailResponse), Lamson tries to create the email it wants to receive by canonicalizing it:

  1. All email will be encoded in the simplest cleanest way possible without losing information.
  2. All headers are converted to 'ascii’, and if that doesn’t work, then 'utf-8’.
  3. All text/* attachments and bodies are converted to ascii, and if that doesn’t work, 'utf-8’.
  4. All other attachments are left alone.
  5. All email addresses are normalized and encoded if they have not been already.

Neat Tricks

The end result of this is that you can now take any email and then convert it to any modern data format and send it over new protocols. For example, here’s the code from librelist.com to implement the JSON dump for the archive browser Javascript interface:

def json_encoding(base):
    ctype, ctp = base.content_encoding['Content-Type']
    cdisp, cdp = base.content_encoding['Content-Disposition']
    ctype = ctype or "text/plain"
    filename = ctp.get('name',None) or cdp.get('filename', None)

    if ctype.startswith('text') or ctype.startswith('message'):
        encoding = None
    else:
        encoding = "base64"

    return {'filename': filename, 'type': ctype, 'disposition': cdisp,
            'format': encoding}

def json_build(base):
    data = {'headers': base.headers,
                'body': base.body,
                'encoding': json_encoding(base),
                'parts': [json_build(p) for p in base.parts],
            }

    if data['encoding']['format'] and base.body:
        data['body'] = base64.b64encode(base.body)

    return data

def to_json(base):
    return json.dumps(json_build(base), sort_keys=True, indent=4)

Since this code can assume that everything inside Lamson is fully Unicode, it only needs to worry about binary items like images and encode those as base64.

lamson cleanse

You can also use the lamson cleanse command to take a Maildir or mbox as input, and then convert it into a cleaned up Maildir as output. Try it on some of your spam folders to watch the magic.

Reporting Problems

If you happen to run into a message that you feel Lamson should accurately decode, feel free to report it and I’ll take a look.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/unicode_encoding_and_decoding.txt0000644000076500000240000001443111243702515030265 0ustar zedshawstaffTitle: Unicode Encoding And Decoding The world is Unicode, but email existed long before that world. Lamson uses special encoding/decoding gear that takes just about any nasty horrible pre-Unicode email it gets and converts it to a perfect little Unicode fantasy. You do all your work in the Unicode world, which Python favors, and then when you're ready to send, Lamson intelligently figures out how to make an email that anyone can read. The added advantage of doing this conversion is that Lamson by default also cleans up all the various tricks spammers use to get around spam checkers. Because it's doing a conversion from just first principles of "everything must become Unicode" the end result is a crystal clear piece of data you can filter. When you then output the same message, it gets reconverted to sane easily parseable message. Lamson is also so persistent in this conversion that it can convert nearly every message that's valid, and the very tiny percentage it can't (less 1/1000th of a percent) are entirely screwed up spam that should not be transmitted anyway. bq. This gear apparently doesn't support pre-Hindic sanskrit, but then again neither does Python. h2. Why Sometimes people ask why Lamson would bother converting everything to Unicode? The reason is simple: everything you deal with now is Unicode. Either it's in a representation like UTF-8, or it's internally a Python Unicode string. Databases, web frameworks, ORM, network protocols, and the entire internet is Unicode. However, there's another practical reason. If you were to start using Lamson to process email, you would end up inventing almost the exact same gear to convert headers and bodies to Unicode. The only difference is you would do it in a typical hacker half-assed fashion that only solved your immediate problems, and you wouldn't do it in a global useful way. The Lamson encoding gear is basically what you should be making to deal with email in a modern language. h2. How The "lamson.encoding":http://lamsonproject.org/docs/api/lamson.encoding-module.html module is the main meat of this conversion magic. This code is probably the most dense part of Lamson since it has to use various parsing tricks and heuristics to figure out how to get every part of the message into a Unicode string. The primary trick used is that Lamson does not trust anything it's given, and when python fails to convert a header or body, it uses the wonderful "chardet":http://chardet.feedparser.org/ library to guess at what the encoding should be based on the contents. If this fails then the entire data is considered suspect and the message is rejected. This turns out to be surprisingly accurate in practice, and it even corrects some invalid clients that fail to encode Subject lines but still place Chinese encodings in them. Lamson will take those unencoded headers, fail to convert them to ascii, run chardet on them to see they are actually Chinese, and then convert them accurately. h2. The Rules A set of axioms controls how the encoding is done: # NO ENCODING IS TRUSTED, NO LANGUAGE IS SACRED, ALL ARE SUSPECT. # Python wants Unicode, it will get Unicode. # Any email that CANNOT become Unicode, CANNOT be processed by Lamson or Python. # Email addresses are ESSENTIAL to Lamson's routing and security, and therefore will be canonicalized and properly encoded. # Lamson will therefore try to "upgrade" all email it receives to Unicode internally, and cleaning all email addresses. # It does this by decoding all codecs, and if the codec LIES, then it will attempt to statistically detect the codec using chardet. # If it can't detect the codec, and the codec lies, then the email is bad. # All text bodies and attachments are then converted to Python Unicode in the same way as the headers. # All other attachments are converted to raw strings as-is. Once Lamson has done this, your Python handler can now assume that all MailRequest objects are happily Unicode enabled and ready to go. The rule is: IF IT CANNOT BE UNICODE, THEN PYTHON CANNOT WORK WITH IT. On the outgoing end (when you send a MailResponse), Lamson tries to create the email it wants to receive by canonicalizing it: # All email will be encoded in the simplest cleanest way possible without losing information. # All headers are converted to 'ascii', and if that doesn't work, then 'utf-8'. # All text/* attachments and bodies are converted to ascii, and if that doesn't work, 'utf-8'. # All other attachments are left alone. # All email addresses are normalized and encoded if they have not been already. h2. Neat Tricks The end result of this is that you can now take any email and then convert it to any modern data format and send it over new protocols. For example, here's the code from "librelist.com":http://librelist.com/ to implement the JSON dump for the "archive browser":http://librelist.com/browser/ Javascript interface:
def json_encoding(base):
    ctype, ctp = base.content_encoding['Content-Type']
    cdisp, cdp = base.content_encoding['Content-Disposition']
    ctype = ctype or "text/plain"
    filename = ctp.get('name',None) or cdp.get('filename', None)

    if ctype.startswith('text') or ctype.startswith('message'):
        encoding = None
    else:
        encoding = "base64"

    return {'filename': filename, 'type': ctype, 'disposition': cdisp,
            'format': encoding}

def json_build(base):
    data = {'headers': base.headers,
                'body': base.body,
                'encoding': json_encoding(base),
                'parts': [json_build(p) for p in base.parts],
            }

    if data['encoding']['format'] and base.body:
        data['body'] = base64.b64encode(base.body)

    return data

def to_json(base):
    return json.dumps(json_build(base), sort_keys=True, indent=4)
Since this code can assume that everything inside Lamson is fully Unicode, it only needs to worry about binary items like images and encode those as base64. h2. lamson cleanse You can also use the @lamson cleanse@ command to take a Maildir or mbox as input, and then convert it into a cleaned up Maildir as output. Try it on some of your spam folders to watch the magic. h2. Reporting Problems If you happen to run into a message that you feel Lamson should accurately decode, feel free to "report it":/contact.html and I'll take a look. lamson-1.0pre11/doc/lamsonproject.org/output/docs/unit_testing.html0000644000076500000240000003754011243070357025164 0ustar zedshawstaff LamsonProject: Unit Testing

Unit Testing

Lamson provides the lamson.testing to help with writing unit tests. It doesn’t do everything for you, but does enough that you can TDD your email interactions with pretend users. It includes features for checking messages get delivered to queues, checking spelling, and running things through a fake or real relay.

The Log Server

The first thing you need to do for testing is to run the “log server”:

$ lamson log

The log server acts as the smart relay host you’ve configured in your config/settings.py file by default. What it does is take all emails that your Lamson application “sends out” and redeposits them into run/queue. This queue directory is then also used by lamson.testing for checking that emails were sent out. When anything goes wrong, you can look in this directory and see what is getting sent out.

An alternative to this setup would be to have all of the sending/routing/relaying done internal to the whole testing framework, similar to how lamson.testing.RelayConversation works. However, I found that this made testing that your server actually sends proper emails much too difficult.

Test Organization

Lamson organizes tests into directories that match the things you’re testing:

librelist $ ls -l tests/
total 16
drwxr-xr-x  6 zedshaw  staff   204 Aug 19 17:01 handlers
drwxr-xr-x  8 zedshaw  staff   272 Aug 18 15:50 model
drwxr-xr-x  3 zedshaw  staff   102 Aug 18 15:50 templates

In most of the projects I’ve only rarely used template tests, but I’ll cover them below. Model tests make sure that any app.model classes work right, and handler tests make sure that any app.handler classes work. Nice and simple.

You can add any other directories you want to this, and you can also use doctests if you want. This comes free with nosetests and is very handy.

Handler Tests

We’ll use an example from the librelist.com code base that validates that a user can subscribe, unsubscribe, and post a message to a mailing list. This test was primarily written in a TDD style, since generally interactions and usability testing works better TDD style.

First you have a common preamble of modules that you need to include:

from nose.tools import *
from lamson.testing import *
from config import settings
import time
from app.model import archive, confirmation

Right away you notice we just include everything from nose.tools and lamson.testing so that we can use it directly. Yes, this violates Python style guidelines, but practicality is more important than dogmatic slavery to supposed standards.

After that we include the config.settings, and two modules from app.model that we’ll use to check that everything was working.

Notice that we don’t include anything from app.handlers directly. These tests are meant to be from the perspective of a user interacting with the handler via emails.

Once we have that we do a little setup to clear set some common variables and clear out some queues we’ll need to check:

queue_path = archive.store_path('test.list', 'queue')
sender = "sender-%s@sender.com" % time.time()
host = "librelist.com"
list_name = "test.list"
list_addr = "test.list@%s" % host
client = RouterConversation(sender, 'Admin Tests')

def setup():
    clear_queue("run/posts")
    clear_queue("run/spam")

Most of these are just variables used in tests later, but the big one is the client variable. It’s a lamson.testing.RouterConversation class that lets you simulate delivering email to your Lamson project.

There’s also a lamson.testing.TestConversation class that actually uses your real config/settings.py to connect to a Relay. This isn’t used so much, but is intended for running “smoke tests” against a newly deployed server.

With that we’re ready to write out first handler test:

def test_new_user_subscribes_with_invalid_name():
    client.begin()

    client.say('test-list@%s' % host, "I can't read!", 'noreply')
    client.say('test=list@%s' % host, "I can't read!", 'noreply')
    clear_queue()

    client.say('unbounce@%s' % host, "I have two email addresses!")
    assert not delivered('noreply')
    assert not delivered('unbounce')

    client.say('noreply@%s' % host, "Dumb dumb.")
    assert not delivered('noreply')

This is the longest of the tests, and shows all the various things you can do with the lamson.testing gear. Here’s what we’re doing in order:

  1. Call client.begin to clear out queues and state and start fresh.
  2. Use client.say to send an email from that client to your Lamson application. Notice that you configured the RelayConversation to pretend to be one person with each email getting the same subject line.
  3. Use lamson.testing.clear_queue when you want to make sure the queue is clean.
  4. Use lamson.testing.delivered to check if a certain message from someone is in the queue.

With that you can do pretty much everything you need to send an email and make sure you get proper replies.

Here’s another example:

def test_new_user_subscribes():
    client.begin()
    msg = client.say(list_addr, "Hey I was wondering how to fix this?",
                     list_name + '-confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')
    clear_queue()

Notice in this example we have a fourth parameter list_name + '-confirm' and we get a msg back from our call to client.say. This basically combines client.say with delivered to do it in one shot. Very commonly, you’ll want to say something to your server and make sure you got a certain response, and then do something with that response. This is how you do that.

We then use this '-confirm’ email message to actually subscribe the fake user.

Finally, here’s two more examples:

def test_existing_user_unsubscribes():
    test_new_user_subscribes()
    msg = client.say(list_name + "-unsubscribe@%s" % host,
        "I would like to unsubscribe.", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed yes I want out.', 'noreply')

def test_existing_user_posts_message():
    test_new_user_subscribes()
    msg = client.say(list_addr, "Howdy folks, I was wondering what this is?",
                     list_addr)
    # make sure it gets archived
    assert delivered(list_addr, to_queue=queue(queue_path))

In test_existing_user_unsubscribes what we do is call test_new_user_subscribes to go through that process again, and then we chain off that to do an unsubscribe. There’s really nothing new here other than that little trick.

In test_existing_user_posts_message we do the usual send a message and expect a reply, but then we also make sure that this message was delivered to the archiver queue.

Apart from those methods and techniques, there’s really nothing more to doing a handler test. The only additional thing would be using assert_in_state to make sure that your handler is in a particular state. I’d recommend against doing that too much in a handler test, since it will make your tests brittle. I only do it when the state is very important, such as when checking that they are in a SPAMMING or BOUNCING state that I need to enforce.

Model Tests

There’s less functionality available in lamson.testing for doing your models. The theory is that your models will be classes, modules, and ORM that you need to perform the majority of your storage and analysis. Since has very little to do with email you probably won’t use lamson.testing as much.

About the only things you might use are APIs for checking that queues get certain messages in them, and that certain users are in certain states.

Here’s a quick example from librelist.com again that tests how archives work:

from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest, MailResponse
from app.model import archive, mailinglist
import simplejson as json
import shutil

queue_path = archive.store_path('test.list', 'queue')
json_path = archive.store_path('test.list', 'json')

def setup():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def teardown():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def test_archive_enqueue():
    msg = MailResponse(From="zedshaw@zedshaw.com", To="test.list@librelist.com",
                       Subject="test message", Body="This is a test.")

    archive.enqueue('test.list', msg)
    assert delivered('zedshaw', to_queue=queue(queue_path))

This is the usual initial setup, and then some extras to make sure that the JSON archives is working. You’ll notice that we hand construct various messages, call methods on the app.model.archive module, and then use delivered to make sure they’re correctly delivered.

Template Tests

Typically you really can only test that your templates are spelled right, or that your templates render when given certain locals. I’ve found that automated testing of templates isn’t incredibly useful yet, so the only one I’ve written is from the oneshotblog example:

from nose.tools import *
from lamson.testing import *
from lamson import view
import os
from glob import glob

def test_spelling():
    message = {}
    original = {}
    for path in glob("app/templates/mail/*.msg"):
        template = "mail/" + os.path.basename(path)
        result = view.render(locals(), template)
        spelling(template, result)

This uses lamson.testing.spelling to make sure that each template renders and that it is spelled correctly. This uses PyEnchant to do the checking, which turns out to be rather annoying. If you are interested in improving the template testing setup, then feel free to talk about your ideas on the lamson mailing list (but bring code, talk is cheap).

Conclusion

Hopefully you’ll be able to develop your application using good testing techniques with the lamson.testing API. If you find additional testing patterns that could be included then talk about them on the lamson mailing list to see if they’re general enough for others.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/unit_testing.txt0000644000076500000240000002525111243070347025032 0ustar zedshawstaffTitle: Unit Testing Lamson provides the "lamson.testing":http://lamsonproject.org/docs/api/lamson.testing-module.html to help with writing unit tests. It doesn't do everything for you, but does enough that you can TDD your email interactions with pretend users. It includes features for checking messages get delivered to queues, checking spelling, and running things through a fake or real relay. h2. The Log Server The first thing you need to do for testing is to run the "log server":
$ lamson log
The log server acts as the smart relay host you've configured in your @config/settings.py@ file by default. What it does is take all emails that your Lamson application "sends out" and redeposits them into @run/queue@. This queue directory is then also used by @lamson.testing@ for checking that emails were sent out. When anything goes wrong, you can look in this directory and see what is getting sent out. bq. An alternative to this setup would be to have all of the sending/routing/relaying done internal to the whole testing framework, similar to how @lamson.testing.RelayConversation@ works. However, I found that this made testing that your server actually sends proper emails much too difficult. h2. Test Organization Lamson organizes tests into directories that match the things you're testing:
librelist $ ls -l tests/
total 16
drwxr-xr-x  6 zedshaw  staff   204 Aug 19 17:01 handlers
drwxr-xr-x  8 zedshaw  staff   272 Aug 18 15:50 model
drwxr-xr-x  3 zedshaw  staff   102 Aug 18 15:50 templates
In most of the projects I've only rarely used template tests, but I'll cover them below. Model tests make sure that any @app.model@ classes work right, and handler tests make sure that any @app.handler@ classes work. Nice and simple. You can add any other directories you want to this, and you can also use doctests if you want. This comes free with "nosetests":http://somethingaboutorange.com/mrl/projects/nose/0.11.1/ and is very handy. h2. Handler Tests We'll use an example from the "librelist.com":http://librelist.com/ code base that validates that a user can subscribe, unsubscribe, and post a message to a mailing list. This test was primarily written in a TDD style, since generally interactions and usability testing works better "TDD":http://en.wikipedia.org/wiki/Test-driven_development style. First you have a common preamble of modules that you need to include:
from nose.tools import *
from lamson.testing import *
from config import settings
import time
from app.model import archive, confirmation

Right away you notice we just include everything from @nose.tools@ and @lamson.testing@ so that we can use it directly. Yes, this violates Python style guidelines, but practicality is more important than dogmatic slavery to supposed standards. After that we include the @config.settings@, and two modules from @app.model@ that we'll use to check that everything was working. bq. Notice that we don't include anything from @app.handlers@ directly. These tests are meant to be from the perspective of a user interacting with the handler via emails. Once we have that we do a little setup to clear set some common variables and clear out some queues we'll need to check:
queue_path = archive.store_path('test.list', 'queue')
sender = "sender-%s@sender.com" % time.time()
host = "librelist.com"
list_name = "test.list"
list_addr = "test.list@%s" % host
client = RouterConversation(sender, 'Admin Tests')


def setup():
    clear_queue("run/posts")
    clear_queue("run/spam")
Most of these are just variables used in tests later, but the big one is the @client@ variable. It's a "lamson.testing.RouterConversation":http://lamsonproject.org/docs/api/lamson.testing.RouterConversation-class.html class that lets you simulate delivering email to your Lamson project. bq. There's also a "lamson.testing.TestConversation":http://lamsonproject.org/docs/api/lamson.testing.TestConversation-class.html class that actually uses your real @config/settings.py@ to connect to a Relay. This isn't used so much, but is intended for running "smoke tests" against a newly deployed server. With that we're ready to write out first handler test:
def test_new_user_subscribes_with_invalid_name():
    client.begin()

    client.say('test-list@%s' % host, "I can't read!", 'noreply')
    client.say('test=list@%s' % host, "I can't read!", 'noreply')
    clear_queue()

    client.say('unbounce@%s' % host, "I have two email addresses!")
    assert not delivered('noreply')
    assert not delivered('unbounce')

    client.say('noreply@%s' % host, "Dumb dumb.")
    assert not delivered('noreply')
This is the longest of the tests, and shows all the various things you can do with the @lamson.testing@ gear. Here's what we're doing in order: # Call @client.begin@ to clear out queues and state and start fresh. # Use @client.say@ to send an email from that client to your Lamson application. Notice that you configured the RelayConversation to pretend to be one person with each email getting the same subject line. # Use @lamson.testing.clear_queue@ when you want to make sure the queue is clean. # Use @lamson.testing.delivered@ to check if a certain message from someone is in the queue. With that you can do pretty much everything you need to send an email and make sure you get proper replies. Here's another example:
def test_new_user_subscribes():
    client.begin()
    msg = client.say(list_addr, "Hey I was wondering how to fix this?",
                     list_name + '-confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')
    clear_queue()
Notice in this example we have a fourth parameter @list_name + '-confirm'@ and we get a @msg@ back from our call to @client.say@. This basically combines @client.say@ with @delivered@ to do it in one shot. Very commonly, you'll want to say something to your server and make sure you got a certain response, and then do something with that response. This is how you do that. We then use this '-confirm' email message to actually subscribe the fake user. Finally, here's two more examples:
def test_existing_user_unsubscribes():
    test_new_user_subscribes()
    msg = client.say(list_name + "-unsubscribe@%s" % host,
        "I would like to unsubscribe.", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed yes I want out.', 'noreply')

def test_existing_user_posts_message():
    test_new_user_subscribes()
    msg = client.say(list_addr, "Howdy folks, I was wondering what this is?",
                     list_addr)
    # make sure it gets archived
    assert delivered(list_addr, to_queue=queue(queue_path))
In @test_existing_user_unsubscribes@ what we do is call @test_new_user_subscribes@ to go through that process again, and then we chain off that to do an unsubscribe. There's really nothing new here other than that little trick. In @test_existing_user_posts_message@ we do the usual send a message and expect a reply, but then we *also* make sure that this message was delivered to the archiver queue. Apart from those methods and techniques, there's really nothing more to doing a handler test. The only additional thing would be using "assert_in_state":http://lamsonproject.org/docs/api/lamson.testing-module.html#assert_in_state to make sure that your handler is in a particular state. I'd recommend against doing that too much in a handler test, since it will make your tests brittle. I only do it when the state is very important, such as when checking that they are in a SPAMMING or BOUNCING state that I need to enforce. h2. Model Tests There's less functionality available in @lamson.testing@ for doing your models. The theory is that your models will be classes, modules, and ORM that you need to perform the majority of your storage and analysis. Since has very little to do with email you probably won't use @lamson.testing@ as much. About the only things you might use are APIs for checking that queues get certain messages in them, and that certain users are in certain states. Here's a quick example from "librelist.com":http://librelist.com again that tests how archives work:
from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest, MailResponse
from app.model import archive, mailinglist
import simplejson as json
import shutil

queue_path = archive.store_path('test.list', 'queue')
json_path = archive.store_path('test.list', 'json')

def setup():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def teardown():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def test_archive_enqueue():
    msg = MailResponse(From="zedshaw@zedshaw.com", To="test.list@librelist.com",
                       Subject="test message", Body="This is a test.")

    archive.enqueue('test.list', msg)
    assert delivered('zedshaw', to_queue=queue(queue_path))
This is the usual initial setup, and then some extras to make sure that the "JSON archives":http://librelist.com/browser/ is working. You'll notice that we hand construct various messages, call methods on the @app.model.archive@ module, and then use @delivered@ to make sure they're correctly delivered. h2. Template Tests Typically you really can only test that your templates are spelled right, or that your templates render when given certain locals. I've found that automated testing of templates isn't incredibly useful yet, so the only one I've written is from the "oneshotblog":http://oneshotblog.com/ example:
from nose.tools import *
from lamson.testing import *
from lamson import view
import os
from glob import glob

def test_spelling():
    message = {}
    original = {}
    for path in glob("app/templates/mail/*.msg"):
        template = "mail/" + os.path.basename(path)
        result = view.render(locals(), template)
        spelling(template, result)
This uses @lamson.testing.spelling@ to make sure that each template renders and that it is spelled correctly. This uses "PyEnchant":http://www.rfk.id.au/software/pyenchant/ to do the checking, which turns out to be rather annoying. If you are interested in improving the template testing setup, then feel free to talk about your ideas on "the lamson mailing list":mailto:lamson@librelist.com (but bring code, talk is cheap). h2. Conclusion Hopefully you'll be able to develop your application using good testing techniques with the @lamson.testing@ API. If you find additional testing patterns that could be included then "talk about them on the lamson mailing list":mailto:lamson@librelist.com to see if they're general enough for others. lamson-1.0pre11/doc/lamsonproject.org/output/docs/writing_a_state_storage.html0000644000076500000240000003255411242714314027354 0ustar zedshawstaff LamsonProject: Writing A Lamson State Storage Backend

Writing A Lamson State Storage Backend

Earlier versions of Lamson assumed that you would use SQLAlchemy, and that you wouldn’t mind storing your state in the database using SQLAlchemy. Well, during the 0.9 redesign it became very easy to let you store the state however and wherever you want. In the new Lamson setup there is a bit more work to create alternative storage, but Lamson comes with two default stores that you can use to get started.

The Default MemoryStore

When you get started with Lamson you’ll definitely not want to go through the trouble of setting up a custom store. For your first days of development, using the default MemoryStorage is the way to go. After you get further in your development you want to switch to the ShelveStorage to store the state in a simple Python shelve store.

MemoryStorage keeps the routing state in a simple dict in memory, and doesn’t provide any thread protection. Its purpose is for developer testing and unit test runs where keeping the state between disks is more of a pain than it is worth. Use the MemoryStorage (which is the default) for your development runs and for simple servers where the state does need to be maintained (very rare).

Here’s the code to MemoryStore for you to just look at, it’s already included in Lamson:

class MemoryStorage(StateStorage):
    """
    The default simplified storage for the Router to hold the states.  This
    should only be used in testing, as you'll lose all your contacts and their
    states if your server shutsdown.  It is also horribly NOT thread safe.
    """
    def __init__(self):
        self.states = {}

    def get(self, module, sender):
        key = self.key(module, sender)
        try:
            return self.states[key]
        except KeyError:
            self.set(module, sender, ROUTE_FIRST_STATE)
            return ROUTE_FIRST_STATE

    def set(self, module, sender, state):
        key = self.key(module, sender)
        self.states[key] = state

    def key(self, module, sender):
        return repr([module, sender])

    def clear(self):
        self.states.clear()

As you can see there isn’t much to implement to make your own storage for Lamson to use.

Keep in mind that this is just the storage Lamson needs to operate, you probably don’t want to be accessing this in your own application, and instead probably want to access the store you create yourself.

The ShelveStorage

ShelveStorage is used for your small deployments where you are mostly just testing the deployment process or doing a small service. It will store your data between runs, and is probably fast enough for most sites, but you’ll want to ditch it if you ever:

  1. Run more than one process that needs the state information.
  2. Start to store everything in a database anyway.

The code to ShelveStorage (which is already part of Lamson) is more complex since it must keep the threads happy, but you should read through it to get an idea of how a more complex state store would work:

class ShelveStorage(MemoryStorage):
    """
    Uses Python's shelve to store the state of the Routers to disk rather than
    in memory like with MemoryStorage.  This will get you going on a small
    install if you need to persist your states (most likely), but if you 
    have a database, you'll need to write your own StateStorage that 
    uses your ORM or database to store.  Consider this an example.
    """
    def __init__(self, database_path):
        """Database path depends on the backing library use by Python's shelve."""
        self.database_path = database_path
        self.lock = threading.RLock()

    def get(self, module, sender):
        """
        This will lock the internal thread lock, and then retrieve from the
        shelf whatever module you request.  If the module is not found then it
        will set (atomically) to ROUTE_FIRST_STATE.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            try:
                key = store[self.key(module, sender)]
            except KeyError:
                self.set(module, sender, ROUTE_FIRST_STATE)
                key = ROUTE_FIRST_STATE
            return key

    def set(self, module, sender, state):
        """
        Acquires the self.lock and then sets the requested state in the shelf.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store[self.key(module, sender)] = state
            store.close()

    def clear(self):
        """
        Primarily used in the debugging/unit testing process to make sure the
        states are clear.  In production this could be a bad thing.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store.clear()
            store.close()

Using The ShelveStorage

You can use ShelveStorage by simply adding this line to your config/boot.py file just before you do anything else with the Router:

from lamson.routing import ShelveStorage
Router.STATE_STORE=ShelveStorage("run/states")

It actually doesn’t matter currently when you do it, but it’s good practice right now.

After you do that, restart lamson and it will start using the new store. Notice that your tests will not use this. It’s not a good idea to have tests use ShelveStorage, but if you want to turn it on for a run to see what happens, then you can modify config/testing.py the same way. You could also write a unit test that did this temporarily by putting that line in your test case.

What To Implement

If you want to implement your own then you just have to implement the methods in StateStorage and make sure it behaves the same as MemoryStorage. Look at the code to ShelveStorage for a moment to see what you need:

class StateStorage(object):
    """
    The base storage class you need to implement for a custom storage
    system.
    """
    def get(self, module, sender):
        """
        You must implement this so that it returns a single string
        of either the state for this combination of arguments, OR
        the ROUTE_FIRST_STATE setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.get.")

    def set(self, module, sender, state):
        """
        Set should take the given parameters and consistently set the state for 
        that combination such that when StateStorage.get is called it gives back
        the same setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.set.")

    def clear(self):
        """
        This should clear ALL states, it is only used in unit testing, so you 
        can have it raise an exception if you want to make this safer.
        """
        raise NotImplementedError("You have to implement a StateStorage.clear for unit testing to work.")

There really isn’t much to it, just methods to get and set based on the module and sender’s email address. Also notice that you don’t have to make it readable in any complete sense, since Lamson doesn’t do anything other than get, set, and clear the state store (and it only clears on reloads and in testing).

Important Considerations

I am purposefully not telling you how to exactly implement it because I’m not exactly sure what is needed as of the 0.9 release. I use the the ShelveStorage, but I’d like to hear what other people have written and then start building infrastructure to make that easier.

There are some important things to consider when you implement your storage though:

  1. Make sure that the calls to all methods are thread safe, and potentially process safe.
  2. If you do thread locking, use the with statement and an RLock.
  3. If your storage is potentially very slow, then consider a caching scheme inside, but write that after making it work correctly.
  4. Do NOT be tempted to store junk in this like it is a “session”. It should be lean and mean and only do state.
  5. Make sure you keep the key being used exactly as given. You can seriously mess up Lamson’s Router if you start getting fancy.

Attaching It To The Router

Your storage backend will then be attached to the lamson.routing.Router in the same way as what you did with ShelveStorage. It really should be that simple since the data stored in the state store is very minimal.

Other Examples

If you want more examples then you can look at the examples/librelist code to see how librelist.com uses Django to store the state.


lamson-1.0pre11/doc/lamsonproject.org/output/docs/writing_a_state_storage.txt0000644000076500000240000002146611242714313027226 0ustar zedshawstaffTitle: Writing A Lamson State Storage Backend Earlier versions of Lamson assumed that you would use SQLAlchemy, and that you wouldn't mind storing your state in the database using SQLAlchemy. Well, during the 0.9 redesign it became very easy to let you store the state however and wherever you want. In the new Lamson setup there is a bit more work to create alternative storage, but Lamson comes with two default stores that you can use to get started. h2. The Default MemoryStore When you get started with Lamson you'll definitely not want to go through the trouble of setting up a custom store. For your first days of development, using the default "MemoryStorage":http://lamsonproject.org/docs/api/lamson.routing.MemoryStorage-class.html is the way to go. After you get further in your development you want to switch to the "ShelveStorage":http://lamsonproject.org/docs/api/lamson.routing.ShelveStorage-class.html to store the state in a simple Python "shelve":http://docs.python.org/library/shelve.html store. @MemoryStorage@ keeps the routing state in a simple dict in memory, and doesn't provide any thread protection. Its purpose is for developer testing and unit test runs where keeping the state between disks is more of a pain than it is worth. Use the @MemoryStorage@ (which is the default) for your development runs and for simple servers where the state does need to be maintained (very rare). Here's the code to MemoryStore for you to just look at, it's already included in Lamson:
class MemoryStorage(StateStorage):
    """
    The default simplified storage for the Router to hold the states.  This
    should only be used in testing, as you'll lose all your contacts and their
    states if your server shutsdown.  It is also horribly NOT thread safe.
    """
    def __init__(self):
        self.states = {}

    def get(self, module, sender):
        key = self.key(module, sender)
        try:
            return self.states[key]
        except KeyError:
            self.set(module, sender, ROUTE_FIRST_STATE)
            return ROUTE_FIRST_STATE

    def set(self, module, sender, state):
        key = self.key(module, sender)
        self.states[key] = state

    def key(self, module, sender):
        return repr([module, sender])

    def clear(self):
        self.states.clear()
As you can see there isn't much to implement to make your own storage for Lamson to use. bq. Keep in mind that this is just the storage *Lamson* needs to operate, you probably don't want to be accessing this in your own application, and instead probably want to access the store you create yourself. h2. The ShelveStorage @ShelveStorage@ is used for your small deployments where you are mostly just testing the deployment process or doing a small service. It will store your data between runs, and is probably fast enough for most sites, but you'll want to ditch it if you ever: # Run more than one process that needs the state information. # Start to store everything in a database anyway. The code to ShelveStorage (which is *already* part of Lamson) is more complex since it must keep the threads happy, but you should read through it to get an idea of how a more complex state store would work:
class ShelveStorage(MemoryStorage):
    """
    Uses Python's shelve to store the state of the Routers to disk rather than
    in memory like with MemoryStorage.  This will get you going on a small
    install if you need to persist your states (most likely), but if you 
    have a database, you'll need to write your own StateStorage that 
    uses your ORM or database to store.  Consider this an example.
    """
    def __init__(self, database_path):
        """Database path depends on the backing library use by Python's shelve."""
        self.database_path = database_path
        self.lock = threading.RLock()

    def get(self, module, sender):
        """
        This will lock the internal thread lock, and then retrieve from the
        shelf whatever module you request.  If the module is not found then it
        will set (atomically) to ROUTE_FIRST_STATE.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            try:
                key = store[self.key(module, sender)]
            except KeyError:
                self.set(module, sender, ROUTE_FIRST_STATE)
                key = ROUTE_FIRST_STATE
            return key

    def set(self, module, sender, state):
        """
        Acquires the self.lock and then sets the requested state in the shelf.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store[self.key(module, sender)] = state
            store.close()

    def clear(self):
        """
        Primarily used in the debugging/unit testing process to make sure the
        states are clear.  In production this could be a bad thing.
        """
        with self.lock:
            store = shelve.open(self.database_path)
            store.clear()
            store.close()
h2. Using The ShelveStorage You can use @ShelveStorage@ by simply adding this line to your @config/boot.py@ file just before you do anything else with the @Router@:
from lamson.routing import ShelveStorage
Router.STATE_STORE=ShelveStorage("run/states")
It actually doesn't matter currently when you do it, but it's good practice right now. After you do that, restart lamson and it will start using the new store. Notice that your *tests will not use this*. It's not a good idea to have tests use @ShelveStorage@, but if you want to turn it on for a run to see what happens, then you can modify @config/testing.py@ the same way. You could also write a unit test that did this temporarily by putting that line in your test case. h2. What To Implement If you want to implement your own then you just have to implement the methods in "StateStorage":http://lamsonproject.org/docs/api/lamson.routing.StateStorage-class.html and make sure it behaves the same as MemoryStorage. Look at the code to ShelveStorage for a moment to see what you need:
class StateStorage(object):
    """
    The base storage class you need to implement for a custom storage
    system.
    """
    def get(self, module, sender):
        """
        You must implement this so that it returns a single string
        of either the state for this combination of arguments, OR
        the ROUTE_FIRST_STATE setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.get.")

    def set(self, module, sender, state):
        """
        Set should take the given parameters and consistently set the state for 
        that combination such that when StateStorage.get is called it gives back
        the same setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.set.")

    def clear(self):
        """
        This should clear ALL states, it is only used in unit testing, so you 
        can have it raise an exception if you want to make this safer.
        """
        raise NotImplementedError("You have to implement a StateStorage.clear for unit testing to work.")
There really isn't much to it, just methods to get and set based on the module and sender's email address. Also notice that you don't have to make it readable in any complete sense, since Lamson doesn't do anything other than get, set, and clear the state store (and it only clears on reloads and in testing). h2. Important Considerations I am purposefully *not* telling you how to exactly implement it because I'm not exactly sure what is needed as of the 0.9 release. I use the the ShelveStorage, but I'd like to hear what other people have written and then start building infrastructure to make that easier. There are some important things to consider when you implement your storage though: # Make sure that the calls to all methods are thread safe, and potentially process safe. # If you do thread locking, use the *with* statement and an RLock. # If your storage is potentially very slow, then consider a caching scheme inside, but *write that after making it work correctly.* # Do *NOT* be tempted to store junk in this like it is a "session". It should be lean and mean and only do state. # Make sure you keep the key being used exactly as given. You can seriously mess up Lamson's Router if you start getting fancy. h2. Attaching It To The Router Your storage backend will then be attached to the lamson.routing.Router in the same way as what you did with ShelveStorage. It really should be that simple since the data stored in the state store is very minimal. h2. Other Examples If you want more examples then you can look at the @examples/librelist@ code to see how "librelist.com":http://librelist.com/ uses "Django":http://www.djangoproject.com/ to store the state. lamson-1.0pre11/doc/lamsonproject.org/output/download.html0000644000076500000240000001247511230770143023323 0ustar zedshawstaff LamsonProject: Downloading Lamson

Downloading Lamson

You can get the Lamson source, read the source releases documentation to find out how.

For people who can’t click the above link, and who still want the source code, do:

bzr branch lp:lamson

But really you should read the read the source releases documentation to find out how.

Installing With Easy Install

Using easy_install is the easiest way to install, simply run “sudo easy_install lamson” and it will do the work of getting the dependencies and setting everything up.

Installing With setup.py

Once you get the source, you can also use setup.py to install Lamson. First make sure that you have Mako, and nose installed:

$ sudo easy_install sqlalchemy
$ sudo easy_install jinja
$ sudo easy_install nose

If you refuse to use easy_install entirely then it is on you to find these projects and install them how you see best. *Make sure you have the most recent version of all dependencies.*

Then you can use this command to install:

$ python setup.py install

Installing Into A Virtual Env

Read the instructions for installing with virtual env in the documentation section.


lamson-1.0pre11/doc/lamsonproject.org/output/download.txt0000644000076500000240000000241311227340123023162 0ustar zedshawstaffFrom: Zed Title: Downloading Lamson You can get the Lamson *source*, read "the source releases":/releases/ documentation to find out how. For people who can't click the above link, and who still want the source code, do:
bzr branch lp:lamson
But really you should read the read "the source releases":/releases/ documentation to find out how. h2. Installing With Easy Install Using easy_install is the easiest way to install, simply run "sudo easy_install lamson" and it will do the work of getting the dependencies and setting everything up. h2. Installing With setup.py Once you get the source, you can also use setup.py to install Lamson. First make sure that you have *Mako*, and *nose* installed:
$ sudo easy_install sqlalchemy
$ sudo easy_install jinja
$ sudo easy_install nose
If you refuse to use easy_install entirely then it is on you to find these projects and install them how you see best. *Make sure you have the most recent version of all dependencies.* Then you can use this command to install:
$ python setup.py install
h2. Installing Into A Virtual Env Read the instructions "for installing with virtual env":/docs/lamson_virtual_env.html in the "documentation":/docs/ section. lamson-1.0pre11/doc/lamsonproject.org/output/favicon.ico0000644000076500000240000000000011203537656022736 0ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/feed.xml0000644000076500000240000006274411313464306022262 0ustar zedshawstaff Lamson Project(TM)http://lamsonproject.orgLamson The Python SMTP Server and FrameworkSun, 20 Dec 2009 10:11:18 GMTPyRSS2Gen-1.0.0http://blogs.law.harvard.edu/tech/rssSmall Lamson 1.0pre10 Updatehttp://lamsonproject.org/blog/2009-12-13.html<h2><a href="/blog/2009-12-13.html">2009-12-13</a> : Small Lamson 1.0pre10 Update</h2> <p> <p>There was a very minor one line fix that corrects a fairly obnoxious bug in Lamson due to my not understanding how <i>nonzero</i> works.</p></p> <p><a href="/blog/2009-12-13.html">Read more...</a></p> http://lamsonproject.org/blog/2009-12-13.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 1.0pre9 Is Outhttp://lamsonproject.org/blog/2009-12-12.html<h2><a href="/blog/2009-12-12.html">2009-12-12</a> : Lamson 1.0pre9 Is Out</h2> <p> <p><strong>UPDATE</strong>: This used to be an announce for 1.0pre8, but python-daemon apparently has a bug that makes it not honor the <span class="caps">TERM</span> signal (still) so I had to go back to <span class="caps">HUP</span>.</p></p> <p><a href="/blog/2009-12-12.html">Read more...</a></p> http://lamsonproject.org/blog/2009-12-12.htmlSun, 20 Dec 2009 10:05:30 GMTQuick Lamson 1.0pre6 Release, More This Weekendhttp://lamsonproject.org/blog/2009-09-26.html<h2><a href="/blog/2009-09-26.html">2009-09-26</a> : Quick Lamson 1.0pre6 Release, More This Weekend</h2> <p> <p>I put up a quick release of Lamson that fixes a bug in the gen project prototype. I recently switched all the domain names being used in the test suite to &#8220;localhost&#8221; or a similar nonexistant domain name. Why? Well, because some people liked to run the Lamson test suite or their fresh project test suite against a <strong>live</strong> <span class="caps">SMTP</span> server. The test suite would &#8220;send&#8221; mail to test@test.com and my own address, under the assumption that people would use the <code>lamson log</code> test server.</p></p> <p><a href="/blog/2009-09-26.html">Read more...</a></p> http://lamsonproject.org/blog/2009-09-26.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 1.0pre5 Outhttp://lamsonproject.org/blog/2009-09-07.html<h2><a href="/blog/2009-09-07.html">2009-09-07</a> : Lamson 1.0pre5 Out</h2> <p> <p>I just pushed Lamson 1.0pre5 to PyPI for your enjoyment:</p></p> <p><a href="/blog/2009-09-07.html">Read more...</a></p> http://lamsonproject.org/blog/2009-09-07.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 1.0pre4 Out, Lots Of Docs Done, 100% Coveragehttp://lamsonproject.org/blog/2009-08-22.html<h2><a href="/blog/2009-08-22.html">2009-08-22</a> : Lamson 1.0pre4 Out, Lots Of Docs Done, 100% Coverage</h2> <p> <p>I happy to announce probably one of the last few releases before I officially put the 1.0 stamp on Lamson. This last 1% of the things I want to do takes a while, but it really puts a good shine on the project.</p></p> <p><a href="/blog/2009-08-22.html">Read more...</a></p> http://lamsonproject.org/blog/2009-08-22.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 1.0pre2, HTML Email, Standalonehttp://lamsonproject.org/blog/2009-08-03.html<h2><a href="/blog/2009-08-03.html">2009-08-03</a> : Lamson 1.0pre2, HTML Email, Standalone</h2> <p> <p>Lamson 1.0pre2 features two features that might signal the end of the beginning or the beginning of the end, depending on your perspective: <span class="caps">HTML</span> Email and Lamson Standalone. <span class="caps">HTML</span> Email support comes from a new module <a href="http://lamsonproject.org/docs/api/lamson.html-module.html">lamson.html</a> that gives a nice template method to send out <span class="caps">HTML</span> to victims&#8230;uh&#8230;customers. Lamson Standalone will be a way to run Lamson as your customized email server instead of another server like Postfix.</p></p> <p><a href="/blog/2009-08-03.html">Read more...</a></p> http://lamsonproject.org/blog/2009-08-03.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 1.0pre1 Releasedhttp://lamsonproject.org/blog/2009-07-20.html<h2><a href="/blog/2009-07-20.html">2009-07-20</a> : Lamson 1.0pre1 Released</h2> <p> <p>Tonight I&#8217;m releasing Lamson 1.0pre1 with all the latest improvements I&#8217;ve made while making <a href="http://librelist.com/">librelist.com</a> and taking feedback from a few people using Lamson. The goal from now on will be to basically squash bugs and write docs, with only a rare feature or two as I find them needed on projects.</p></p> <p><a href="/blog/2009-07-20.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-20.htmlSun, 20 Dec 2009 10:05:30 GMTI Blame Bounces For The Delayhttp://lamsonproject.org/blog/2009-07-19.html<h2><a href="/blog/2009-07-19.html">2009-07-19</a> : I Blame Bounces For The Delay</h2> <p> <p>I was going to release Lamson 0.9.6 much earlier than this, but as I worked on the bounce detection feature, I realized that if I just wrote that and a few other cleanups and features, I&#8217;d actually have a <strong>1.0pre1</strong> candidate on my hands instead of a 0.9.6 release.</p></p> <p><a href="/blog/2009-07-19.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-19.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.6 Sometime Todayhttp://lamsonproject.org/blog/2009-07-14.html<h2><a href="/blog/2009-07-14.html">2009-07-14</a> : Lamson 0.9.6 Sometime Today</h2> <p> <p>I think I&#8217;m at a point where bounce detection and handling works really well and is ready for release. I&#8217;ve got it running on the latest Lamson demo <a href="http://librelist.com/">librelist.com</a> and it works. I&#8217;ll be pushing out a new release of Libre List in a little while that does some advanced bounce handling, and if that all works, then I&#8217;ll push out 0.9.6.</p></p> <p><a href="/blog/2009-07-14.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-14.htmlSun, 20 Dec 2009 10:05:30 GMTLamson's Bounce Detection Algorithmhttp://lamsonproject.org/blog/2009-07-09.html<h2><a href="/blog/2009-07-09.html">2009-07-09</a> : Lamson's Bounce Detection Algorithm</h2> <p> <p>I just finished committing 0.9.6 code for doing bounce detection and analysis with Lamson. It&#8217;s part of the new mailing list example I&#8217;m coding up for the 0.9.6 release which I&#8217;ll be running on a free mailing list site I&#8217;m going to release soon. In this blog post I&#8217;d like to go through the bounce detection algorithm and get some feedback and samples from people. So far it works great for the samples I have, but I want it to be fairly bullet proof.</p></p> <p><a href="/blog/2009-07-09.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-09.htmlSun, 20 Dec 2009 10:05:30 GMTLamson Mailing Lists Downhttp://lamsonproject.org/blog/2009-07-07.html<h2><a href="/blog/2009-07-07.html">2009-07-07</a> : Lamson Mailing Lists Down</h2> <p> <p>I&#8217;m making up the release of Lamson and doing some server maintenance so I took down the Lamson server running the mailing lists. I&#8217;ll let everyone know when they&#8217;re back up.</p></p> <p><a href="/blog/2009-07-07.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-07.htmlSun, 20 Dec 2009 10:05:30 GMTArticle In The Reg About Lamson (By Ted Dziuba)http://lamsonproject.org/blog/2009-07-03.html<h2><a href="/blog/2009-07-03.html">2009-07-03</a> : Article In The Reg About Lamson (By Ted Dziuba)</h2> <p> <p>Just a quick update to point people at an article in <a href="http://www.theregister.co.uk/">The Register</a> by <a href="http://teddziuba.com/">Ted Dziuba</a> entitled <a href="http://www.theregister.co.uk/2009/07/03/lamson/">Lamson &#8211; email app coding without the palm sweat: Doing what Java never did</a>. He interviewed me about Lamson, things I think you can use it for, and other fun stuffs.</p></p> <p><a href="/blog/2009-07-03.html">Read more...</a></p> http://lamsonproject.org/blog/2009-07-03.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.5, The Push To 1.0http://lamsonproject.org/blog/2009-06-26.html<h2><a href="/blog/2009-06-26.html">2009-06-26</a> : Lamson 0.9.5, The Push To 1.0</h2> <p> <p>I just released Lamson 0.9.5 with all the major Unicode refactoring done and working. This is an important release because 0.9.5 is where I declare that I&#8217;m pushing to a 1.0 release and the base Lamson <a href="/docs/api/">APIs</a> won&#8217;t change under penalty of death. In fact they can&#8217;t change because I&#8217;m using them myself in a few applications.</p></p> <p><a href="/blog/2009-06-26.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-26.htmlSun, 20 Dec 2009 10:05:30 GMT0.9.5 Almost There, But Stumped On Templateshttp://lamsonproject.org/blog/2009-06-22.html<h2><a href="/blog/2009-06-22.html">2009-06-22</a> : 0.9.5 Almost There, But Stumped On Templates</h2> <p> <p>Since the 0.9.4 release I&#8217;ve rewritten the main part of the decoding parser so that it&#8217;s much cleaner and handles more edge conditions. If there&#8217;s one word that defines what makes <span class="caps">MIME</span> horrible it would be &#8220;edge&#8221;.</p></p> <p><a href="/blog/2009-06-22.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-22.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.4 With Unicode Super Powershttp://lamsonproject.org/blog/2009-06-20.html<h2><a href="/blog/2009-06-20.html">2009-06-20</a> : Lamson 0.9.4 With Unicode Super Powers</h2> <p> <p>Lamson 0.9.4 is out and it&#8217;s sporting a completely rewritten and meticulously crafted encoding system. With the new <a href="http://lamsonproject.org/docs/api/lamson.encoding-module.html">lamson.encoding</a> code Lamson can now decode nearly any nasty horrible encoded spam or mail you hand it, turn it into pristine nice Python unicode strings, and then output sweet clean ascii or utf-8 in a consistent way.</p></p> <p><a href="/blog/2009-06-20.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-20.htmlSun, 20 Dec 2009 10:05:30 GMTThe Mailocalypse Is Upon Us!http://lamsonproject.org/blog/2009-06-14.html<h2><a href="/blog/2009-06-14.html">2009-06-14</a> : The Mailocalypse Is Upon Us!</h2> <p> <p>I&#8217;m currently polishing off the two final features before I start going for the Lamson 1.0 release. I&#8217;ve been using Lamson to make a few little cute applications and create one thing for a potential client, and so far I haven&#8217;t had to change much since 0.9.3. It&#8217;s great so far and I hope that Lamson 1.0 will be a fun release.</p></p> <p><a href="/blog/2009-06-14.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-14.htmlSun, 20 Dec 2009 10:05:30 GMTLamson At NYLUG Python Workshop Today @ 6:00PMhttp://lamsonproject.org/blog/2009-06-09.html<h2><a href="/blog/2009-06-09.html">2009-06-09</a> : Lamson At NYLUG Python Workshop Today @ 6:00PM</h2> <p> <p>Just a quick reminder that I&#8217;ll be presenting <a href="http://lamsonproject.org/">Lamson</a> to the <span class="caps">NYLUG</span> Python Workshop today at 6:00PM. The event is at the NY Public Library Hudson Park Branch, 66 Leroy St., NY NY 10014 in <span class="caps">NYC</span> and you can <a href="http://nylug.org/pipermail/nylug-announce/2009-June/000795.html">find out more here.</a></p></p> <p><a href="/blog/2009-06-09.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-09.htmlSun, 20 Dec 2009 10:05:30 GMTA Screencast And Docs On Deploying Lamson And OneShotBloghttp://lamsonproject.org/blog/2009-06-08.html<h2><a href="/blog/2009-06-08.html">2009-06-08</a> : A Screencast And Docs On Deploying Lamson And OneShotBlog</h2> <p> <p>I just finished writing some new documentation on <a href="/docs/deploying_lamson.html">Deploying Lamson and OneShotBlog</a> that shows you how to install Lamson and all required software into a completely clean Python 2.6 and virtualenv configuration. The instructions take you from nothing to a running <a href="http://oneshotblog.com/">oneshotblog.com</a> installation that you can play with and hack on. The process of doing Lamson deployments is still a little too rough for me, but these instructions should get people started.</p></p> <p><a href="/blog/2009-06-08.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-08.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.3 Is Out And Sexy As Hellhttp://lamsonproject.org/blog/2009-06-06.html<h2><a href="/blog/2009-06-06.html">2009-06-06</a> : Lamson 0.9.3 Is Out And Sexy As Hell</h2> <p> <p>This release is the result of me working on my little <a href="http://oneshotblog.com/">oneshotblog.com</a> project while tweaking and refining Lamson as I go. The end result is 0.9.3 didn&#8217;t have a lot of big code changes, but all the tiny little changes add up to a very nice release. The highlights of this release are more secure server runs, better character encoding handling for headers, various cleanups in how mail is queued, and fixes for Python 2.6 support.</p></p> <p><a href="/blog/2009-06-06.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-06.htmlSun, 20 Dec 2009 10:05:30 GMTOneShotBlog Sample (Hack) Runninghttp://lamsonproject.org/blog/2009-06-04.html<h2><a href="/blog/2009-06-04.html">2009-06-04</a> : OneShotBlog Sample (Hack) Running</h2> <p> <p>I <strong>finally</strong> got off my ass and put the <a href="http://oneshotblog.com/">OneShotBlog</a> sample up. This is the code (plus a few little tweaks) from the sample that is in the <a href="/releases/">Lamson source</a> running on another server. It&#8217;s using all the features of Lamson, include the new <a href="/docs/deferred_processing_to_queues.html">Queue Receiver</a> functionality. So far it&#8217;s working great considering I&#8217;ve just been hacking on it on the side to try out the usability of the Lamson <a href="/docs/api/">APIs</a> and not really taken it seriously.</p></p> <p><a href="/blog/2009-06-04.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-04.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.1 Out, New Docshttp://lamsonproject.org/blog/2009-06-03.html<h2><a href="/blog/2009-06-03.html">2009-06-03</a> : Lamson 0.9.1 Out, New Docs</h2> <p> <p>I released Lamson 0.9.1 today so please <a href="/download.html">grab it and test it</a> and shoot me feedback.</p></p> <p><a href="/blog/2009-06-03.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-03.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9.2, Test Coverage 97%http://lamsonproject.org/blog/2009-06-03-2.html<h2><a href="/blog/2009-06-03-2.html">2009-06-03-2</a> : Lamson 0.9.2, Test Coverage 97%</h2> <p> <p>The 0.9.2 release is out and ready for everyone to easy_install. I spent the day getting rid of my tech debt by boosting the Lamson test coverage to a whopping 97%.</p></p> <p><a href="/blog/2009-06-03-2.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-03-2.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9 Is Out, Find My Bugs!http://lamsonproject.org/blog/2009-06-01.html<h2><a href="/blog/2009-06-01.html">2009-06-01</a> : Lamson 0.9 Is Out, Find My Bugs!</h2> <p> I just pushed Lamson 0.9 up to PyPI for everyone to grab and break. This release features a complete redesign of the routing, state handling, templating, and a full set of very complete documentation. Everyone who was using 0.8.x series should be able to migrate to this version with some work, but it won&#8217;t be terribly painful (assuming you have unit tests).</p> <p><a href="/blog/2009-06-01.html">Read more...</a></p> http://lamsonproject.org/blog/2009-06-01.htmlSun, 20 Dec 2009 10:05:30 GMTLamson 0.9 Later Todayhttp://lamsonproject.org/blog/2009-05-31.html<h2><a href="/blog/2009-05-31.html">2009-05-31</a> : Lamson 0.9 Later Today</h2> <p> <p>I have been working hard on the documentation and scrubbing the code for Lamson and the 0.9 release coming out soon. The only things I feel I need to do before an official 0.9 release are:</p></p> <p><a href="/blog/2009-05-31.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-31.htmlSun, 20 Dec 2009 10:05:30 GMT0.9-pre2 Up For Testing, Docs Toohttp://lamsonproject.org/blog/2009-05-28.html<h2><a href="/blog/2009-05-28.html">2009-05-28</a> : 0.9-pre2 Up For Testing, Docs Too</h2> <p> <p>First off, my apologies to everyone if your <span class="caps">RSS</span> reader went crazy today. I include documentation changes in the <span class="caps">RSS</span> feed so that people can easily track updates to the Lamson docs. However, that means when I&#8217;m writing a lot of documentation it hits the feed repeatedly.</p></p> <p><a href="/blog/2009-05-28.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-28.htmlSun, 20 Dec 2009 10:05:30 GMTFeatures For The 0.9 Release (Soon)http://lamsonproject.org/blog/2009-05-24.html<h2><a href="/blog/2009-05-24.html">2009-05-24</a> : Features For The 0.9 Release (Soon)</h2> <p> <p>I've been hard at work cooking up the very nice new routing system, and I must say it is rather tasty. I've gone and created a whole new routing and state management design that uses decorators right in your handler modules to indicate how each state will expect mail addresses. </p></p> <p><a href="/blog/2009-05-24.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-24.htmlSun, 20 Dec 2009 10:05:30 GMTLamson Project Ideashttp://lamsonproject.org/blog/2009-05-20.html<h2><a href="/blog/2009-05-20.html">2009-05-20</a> : Lamson Project Ideas</h2> <p> <p>I wrote a <a href="http://zedshaw.com/blog/2009-05-20.html">blog post about project ideas for Lamson</a> on my personal blog. Head on over if you&#8217;re looking for something to hack on, or just want something to read that isn&#8217;t about the web.</p></p> <p><a href="/blog/2009-05-20.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-20.htmlSun, 20 Dec 2009 10:05:30 GMTNew Site Look, Same Great Contenthttp://lamsonproject.org/blog/2009-05-19.html<h2><a href="/blog/2009-05-19.html">2009-05-19</a> : New Site Look, Same Great Content</h2> <p> <p>This is just a quick update to say thanks to <a href="http://kenkeiter.com/">Ken Keiter</a> for creating a new <a href="http://lamsonproject.org/">lamsonproject.org</a> site layout and design. The new site should be easier to read, have more breathing room, and look easier on the eyes. It&#8217;s even got a logo:</p></p> <p><a href="/blog/2009-05-19.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-19.htmlSun, 20 Dec 2009 10:05:30 GMTBug Fix 0.8.4, Mailing Lists, Spam Blockinghttp://lamsonproject.org/blog/2009-05-18.html<h2><a href="/blog/2009-05-18.html">2009-05-18</a> : Bug Fix 0.8.4, Mailing Lists, Spam Blocking</h2> <p> <p>A few announcements from my work on Lamson the last few days. I managed to fix a bug, put Lamson to work doing Lamson&#8217;s mailing lists, and use Lamson to do some spam blocking on my own email account.</p></p> <p><a href="/blog/2009-05-18.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-18.htmlSun, 20 Dec 2009 10:05:30 GMTLamson Project Site Launchedhttp://lamsonproject.org/blog/2009-05-16.html<h2><a href="/blog/2009-05-16.html">2009-05-16</a> : Lamson Project Site Launched</h2> <p> <p>Today I launched the Lamson Project site at <a href="http://lamsonproject.org/">lamsonproject.org</a> and started filling in the content. Lamson is really turning into a fun and useful project, and hopefully the site will get other people interested in it and using it.</p></p> <p><a href="/blog/2009-05-16.html">Read more...</a></p> http://lamsonproject.org/blog/2009-05-16.htmlSun, 20 Dec 2009 10:05:30 GMTlamson-1.0pre11/doc/lamsonproject.org/output/home_template.html0000644000076500000240000001231711230770103024326 0ustar zedshawstaff LamsonProject: $title

Features

  • Avoid aliases forever! Lamson uses friendly regular expressions and FSM-based routing.

  • Use an RDBMS and ORM instead of a bizarre combination of flat-files and hashtables!

  • Get up and running quickly. Lamson can be installed and running in 30 seconds.

Get Started »

The Python SMTP Server

We've all been there, mucking around in the sendmail m4 macros trying one more time to get the damn mailing list to update for the new users. Every time we say, "This sucks, I want to rewrite this stupid thing." Yet, when we're done, we simply crawl back to our caves covered in our sendmail wounds.

Lamson's goal is to put an end to the hell that is "e-mail application development". Rather than stay stuck in the 1970s, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

$title

$content

lamson-1.0pre11/doc/lamsonproject.org/output/images/0000755000076500000240000000000011313464574022074 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/images/bg.gif0000644000076500000240000000151511167205414023146 0ustar zedshawstaffGIF89a!,LQQ 3R 7OOBB+V ==/E??-H))$NCA` *\XÇ#6A"3jؤ Cɓ&\˗(ȜIM9r ϟ?4JQ'*]4ӧPJ %Xu+`ÊٳhXvmp}n]x+߿L0*!+^ǎ@Q@˗/h̹CM4hS6ºuװ_Mǃ۸s-߿y.D4(_μ-HNسkߎ}O~ A+Ͽ*;lamson-1.0pre11/doc/lamsonproject.org/output/images/buttons.png0000644000076500000240000000366111204063554024276 0ustar zedshawstaffPNG  IHDRNsBIT|d pHYs  ~tEXtCreation Time5/17/09<@tEXtSoftwareAdobe Fireworks CS4Ӡ IDATx1jFƵ|UR%1K`TR _%m[3i"lvqY+`NeY|sۺ>.˲=:tp\u {.  @@@:t  ,ef;v9.r[:ff޽{w33wn]}۷W3st.3s~a]ml@:t  @@@:t  @@@:t  @@@:t  @@TOIENDB`lamson-1.0pre11/doc/lamsonproject.org/output/images/capbl.gif0000644000076500000240000000027611167311172023641 0ustar zedshawstaffGIF89a~7Qd{Sy/FW Rw%8Gy^ %A_tW}!*(?]q8Sg!,; dihÚ[F2i#D]p h2gs6pN[b iB;lamson-1.0pre11/doc/lamsonproject.org/output/images/capbr.gif0000644000076500000240000000027611167311201023640 0ustar zedshawstaffGIF89a~7Qd{Sy/FW Rw%8Gy^ %A_tW}!*(?]q8Sg!,; dihl;>.)cD @@ܓ@ cTMgju$p(҆d7C;lamson-1.0pre11/doc/lamsonproject.org/output/images/captl.gif0000644000076500000240000000027611167311441023662 0ustar zedshawstaffGIF89a~7Qd(%%8GV{{SyW}y BavA_t8Sg^0HY!*| !,;` d4F92Ϋ O_= :[PileMxL.B;lamson-1.0pre11/doc/lamsonproject.org/output/images/captr.gif0000644000076500000240000001124311167311501023661 0ustar zedshawstaffGIF89a&F~  --- !!!"""%%%&&&(((+++)))...111333666555###'''000888***:::,,,222444===777;;;999???>>>BBB@@@AAACCCGGGMMMRRRXXXHHHEEEKKK]]]IIIVVVQQQDDDOOOFFFYYYTTTcccmmm[[[ggghhh___aaabbbUUUJJJoooPPPsssyyydddjjj\\\eeeNNN~~~tttlllrrrwww{{{qqqzzziiikkkSSSfff```WWW}}}>LU+ٳ D]6p%A.xwMLx0ÈQXl#)TH,+2k^ᢳ:BMz4j.4(B A: h߮w"C‹b#)¾Sy_!4zq"%(h $8A 6XCFERх^҆ #L0dbN9SPAETRI5TTQUEVYqX_EZgŖ[1%]uW_~Wa%cAe]f}Yh:VjlɆm[oq!wr=לtYwvApwy穇^{A9C 7 '* :(APBq*$"$dbM(N,P0"%R4BeT8#aXi [F΅]J՗OBi1fXVv]z hbgllޖqGgv&{J ڝw(z@* $ƛǡ*2Q>TZݬК4MW uEWK]{}.bnd&r[*w7w߁B 0 $|PH b @[>2%kN!\@"Ѭt2YP gp]6;vH];=Z+-eJVJט'6-zv=|eont7b5N@ bwC6QPTD?SB\H@V4]hf nwD7BnŸ́ -y Laz٤Z^6C=b4 B@ȃ">(G))^bE-~¨@2g+ Y#|B: P£ص. c(/5oÞ"55Ap $89)I򔨄*#VID鬌.h# r|:9Jv,!fl-EH4Ym0@d"~Q!h,Ƃ8 fxЉx@ j)ɠ|Fh Ѐd%BAʢձF{]xyAw`+Z̋6)2;Z.gr)4THCIpp r@'Hp 8 e(^N 'QjT J]?TYPcepՅ(v*Fh wF;UklG%ikM81=?? VrƠD4v@ͮv:YTֲH,Gٓ@VERWUȴ5j|ڈvcl25dF([l- )5Mj6< l0Kn%7 nKs$nC.+Vm%9K ^ʽ‚_bVmn5,ӂdM`)l qw߼bVscY07E6KxA?thb[޸;>lS3+L P`lX| [P x=w8\Aqh=ԱN=Az-Y^dU-xx'0D}xi &HA ahDy|$Jr$"($JHI8HD8fmf{8Zwx(3|?xA&GvnJ`X8q11Y6P؈^ҍWEHʇ]xK#,8V ,/iC?J7yhi(Wē y+5 7Pwx4AlaU +NE`)BB Y8O3V6H;X㸔ؔ"0!Е0D04 dYxvg=@xppbp)ɘɛ>$߄ b l?nɜdž(B^uIFUy#xZ @?1CyS {=ٙ YcY|yי#z%I'P9N ҡDZ 9Ej蜠itGgLJ.KP@C:ʣ4P &/P ejm] "ʙ]A I UlWpjfҦ(Ԥ%uw*+cGѧY03b*¨:"ny"2KOjQC 0${6!JZj*9j,᫛Zx@;.Q#dJiЊ6Ҋ ZzL:t' XJۦL۴۫+!!@-h[{_˭ "LTj[vI5b7Ps*&q;xzJE'Aom[N %۸3۹;:W2/󹨛ws븪;׺s$Qk뵠țۂËEoһeؼӛKx!Tۀ˽p*㛾K9it˾Ik4+{o\<&c L* ڦ\&"d "ÍaI d~tuzp?SOTw]=]~j 4Aw4hLtww uArԎ,\^Iyt~_[tT` ipL4p A8Xpƻ\.*Sh$1 w& 7{y0H%hh ID"Vx 7 &@QihH jXy C$hh 7ID4O"A,՜P>(jE$hh HxJ,YS`uDh)GULJ$hh I?eMDX2QDd&9g,xF"}rH2X㧚&< T`Viw[M 'G_SSbՠA"҆|ANY&$t|iUF2np Kj$G4:e)Q;y <&ԑ&* I"<agtXV:F'~I#{ԝDdT?vṥ`ViZΰ$ª?4xޭrl&@s xgJ>!@\͖wIn[ƞC%'}x`}?%$rd~,h Z"M@sH~14xoDH5~E ,lN C~q7U,G*yw笓@hI#jU7{7 i6.i@&@aQkw^=fC]KlAScfdS4H0Ui"W4+u|q?#)&i\m\W~$<MK) 8m6云,VϟTg R?"RE-u$5/l%DNՎ<$*C8Nmұc'={:uBRRbccg,YYYYŒ3Yfǖ-[?Xб!x y~'<2{40C k!FlHGV: fRmIIBsm/I6mյGoAi4$ug.;`$MU%m {y.jeq&:AJy^dllF1G9|=<$_ %k!iğxfE< 6̣ƌ3saɒ%ѣ1o<̛70!8˜3ϿE}a 9 Zm-&rIx?OҙLʈ%h/#C\$7l#!CҎŐ^nrH+ȵ(G"x0RtdgvAȅLp۫2mI-u`cq'yqzu'(c,l{I!HD}$4z)L@?>אfHJJ|^z ӧOŋo_O?qa_?s"MCi_+HF<&H/DI^ԹNi w5 % w)0KL*mI#e̅;QY2,ܾ@2h)a9#THLpo[{#" 1^,K) vJ` Āj30 [ ׺S*ͭOB[7w}:eϣiKbXf &Mcɒ%Xt)‚u8+=ӗwCHS} OI / w.q<՜bN}T>9iʪQ&I8{ [W; 0 GʕUŴBlTu,*T{Ul-l6͖OԥI˛S$M!$4% ¨6.J(sFNgD>8|&_ =z46n8{%0m@  tL^٘ f\%uBH#|J7N]nF[^ 4Q_X^:]=[VQ#WhSnjDRlm#HXMeLdTtMEy왳0CVA[߿}=Z=DDFFӸvʋoU3 F(/{g Id H\>s?J9, @H~&bZa]Wseߎ߯O\['fM JOO^TTT`߾}X~=^}Ut fB۶m\~ѲeKۮy# +c/hBHPG@OkЄ3׏ TDX"!24|a857Z޼~piO9(u" ©QS##kڬv=2e 3gD޽ѻwo\p(bر>}:~zy(W+\111:ر[̚5 E*S 5H`2REQ;$)N@H쐌v(S(3$;K~]>A>!)^ ֈx44zҁ-ηE>h>w[3z^.fFkC׫yp՜;$ֵƏgGF6p:ظq#j*@ll,fϞlL0ׯRRRK/sعs'ZnTGCNx*oɭ :F I21B+03eDPպcWOպ3+DXF!BWmDE>T7rH;vߟu&n`/_9|g/DӰj:XO0 8tTUUlق+V˘?>V^+W"<<& 9v;BCC1hР:?^{ j֬nС=oڏzUާ$B"#xC|Ha:AboV#:7K: D <_A?\t=ӽdӇ¿o}D 0#77۶mÐ!Cc_FZZwcȑ#ZEЯ_? < l>,EUUV:v}{qG}%#ByC85CC!|(kK&R'1d*yZ8ʴ Px׀Z""6I^⡚N{m6[ndܣ Ճo_lc*Aw ͅ PLsrtF 2˱ylݺ${ T#"7| 5ɓ1x`(,,D^? VB-zjXn]m9o6<N2<ƅ-O'U7uȲ`!b8!)"DxD’I]@-Hꥐ^ y:l2!u$<K'{&@"! S!sYCnzwN\-(qlTLྶE~7FCL@ӥzĀA3@D  "{zzs9n?/<Gff&ۇVZᣏ>l۶ =u_=8iӦ!-- 3gĩSЫWM6 oڴ/idٲeXhN>VZ!HwjsL7\3ɛћBD&ҦDh( 䑀&# Tub}@~3B-e}9kC=kֆZ;>N$uϝ6G" .+9sZ8z,o庡F?T̜9999ݻ7`ҥX&?z|#-- Fw}˖-øq ŋ1"k֬V9qx 󥒊ʲfKϡ+v`5Hux dɄVqĖ\Ѡ$r'[ ` @^W^yHII%@2n8VKϟĉq)^J"Xv-\|/wrRv3\ɘw?`C9ܷ?AdUvh0UFsiD#D4/xb&''#66HMMEAAR {Mo2лwo\sٳt EEEnˑ̙˷w,=kb]U =WOSDC!̎,"QF"߽mşY/;vΝ;k{cǎL?f|U !!'Nm6 9jOXǓn6,=kp``x[Ѡi$n` <5tnd^^^P(--E\\>\ߪU+$$<͛7gQMѵkW޽;*++Ag*++QSSa#.:kYhNْk`ոDC`xJjB:6-g \^r:uژ1c+Wxͣ}ؽ{7QUUݜ .yjcMG\2(,wcdեVqyd)S^4%kBoJkʙϞ=y.1118}4nܸvjbڵX`U_霔={ .mS"Á6]F"yMPs,xj5XEj5(SŻxfZ h3M6$g6j5I"v$@{FϲBQQv͛7EǗ_~^xj3@W^EDDJKKX{`Ƿ5B[Bj_֘ʒڔaZ3 s= DQG eQwy5 <5F&`7O*;Lά9s-o/^ڭ[7(**Ƹqc W.\7nF~Ǐbqt"** 'N͛71j(]p̯W1GI"9mImDEj0t|j)ݎx:@E'66M9a,ǫHg@P;<;o$"ɍD%~:}ȇ}RNؼifGSг C} [83̟?aaሎFAA KGII 233qUGÓ8yNd<D VlZ]lPKy(fB)j e"*@FA#yIAB:t19@"\]E Igjy`ۨ׎ll)V+On$*Wn(:5r-@&9V5IDQ4d@"a; gX.K#U@T#~K϶7vz:⥋ҫe1u9xhױ|ؔ_I闚8(r}nOMLr9tJ>=N b}0^s?6&gS' zfXt)zꅇzݻŘ70qD2g믿BB=''/< hҴ |G 50-]ؒkJ #!37M9]Pz!UUz4Bl['~,Z%\4M72 ]z8\.? $ sȄC]7.$fTb"qj߼vY _0zKd^J cSu/4h%6@v6;+% )` z[}HVRI"#%H@9?"aӪI"/L4a+u @ 4FC$DIY,» yCݍHDC@`$~$Kܫv+E3v`4 hhDdK%:\-w/EMok- hh ;P$(䣯8ȃDxGMnVID44:p'l|M2F Zjtm$Jj$~Iw LO_I" ipRRƺ^b-Z˖Q;߸pXtFNr$H(xĉJ21R_#ykC7n1`)Sdy*A Ԃ[X!A ˔)x="H‚$#9 Th8#Ć È$(b0(4hcۈU`%"eidi$F@`UЂ1aD|}ŀ2 i&f 'ƩbƪŬzƭk+E+ű&5DS@4$GfN>ijɶKmՒ[5]~)LlC5 묃1"@gv]{9bz5hP' (q:J ꫱J+k[<1Ttm"]dz kkZ[3 bv0,w (o6o6#*0A #K\EMƩN b .0Jɥ**l+̺135{Π#3L]K2)O&%lvemnw]כ&$CpJ"pbp{)s1z7K8 )N )i& ʓl9`dE衏?>ЪդA)JRnceF] :y)^ؤCfXю ցaax#61q J=#X˔<j}+s\?ͯs+@gG_xZ1iQjhW;=-KQSWps)A1t8PT0J_FIJLgw/윢;wϞ& PRel@}]TS֎5v,9`C@fHbNuC BrL@.Ek.T&Ζy,x69"adZd=hsnK؈`,eymef F>3|<(SAr,8kIh;$wiks{ʎ{/DB9H9@m"D@L6֫Uy<iZiO5gOR>` Vm)' K7BMjq]K85w8IlfwKX| BRk[1i Sʊp a*$gb0e)St^Z0EyL!L!,Xc9^ Yj|~ݚBSVV)hNn!,R8Ԣ*^NFK&&4*Là /,N*0<Q4,zH18c;B[A#7@j[vt7irEúv=\wi"X/iNV-XV#{ti(~/D4HU܇77,.ag\rP]37{Ȉqk>(wb/3{( JpE#t\-}Nwt'dVTθunco~KwKe"-U&׼Xh~ozy!7v7 Z12~Q@dX@i yadWA5k4bX^gZ"<zBohDX a-YTOW.X#KgA]S}0/ <6VU3~p3~S~=x@~iFl(E||Xu'6FFYr&4&b/7(&bWL0x~uǁj؆Ȇ`I%H;ezT/x}rF׀4hh~lWhHa$ȄXc]F&UX4" 6L#Q'teHMܸr4䉟vO,H}/r^ohҲ؏8i\@&%H`} +֊Zȃ~8ؑEy8n8/xP6XBw$g:89U63 C42&y2g0"pu?w9<,*q'1ű2#3$`"(Ρ)흻24}<| ` P("0N/QY@C` _T;cb<8@c0I>h% H#\~(RCh'۸%Σ "V%-#ElSOLPG&+s$ƒ%+Aξ( \u-pס~80\}e(0/~*cdsm/y.aQmkAeՌ60 gt1F” pS8LaUМM!Sb2<1WJ)j^w$BTBXX%t.С)vQ. !AR`6}{lsV ab:zBx4Mp`Y'4Iq,qa`8JxCFow}`hm057ޯAFJy:@C_fk!,c@pTȤlj>!er@RG)8x,(hA$h RA " A;lamson-1.0pre11/doc/lamsonproject.org/output/images/testimg.gif0000644000076500000240000001112511167214303024225 0ustar zedshawstaffGIF89anneH?R- S* _3ݏ\ޓنt-]#x|p=n+yk/Di(AVۋSNc؀DWƴ Iul7ʦuO/5t N -b;K"E 1b93d\3z` CX֬wN5Hul'yӠGt%h)ЀO\|?ԫ^Rg.!p(1l%#?IApiHPо,]<$IUPmT0 AeYaװ5#'hHqHGs(Ő!a Q` c0ah*!8axŘ8~ u2A .4@i8dY Y F%~E:i$Z2a{PcYı>b|+XJ M]`H p`40HHs g3;0nh mQXQG7p n PMh 5R%,rkd'74 A^B ,'kҩz82lFHz^<׹~ uu@4*1~ǰFM}ќ~b1~ >TGqr}ギ1>*LC<(tB]a{04 h"/qЈR)W`pgLB,# ץaT1 h0{YH\bil0ڡbt q'lAjt~ ^Bnc-n~,pٳ>\]kgW@@]/ %`W !*Z@d8,bOu x$ CmVR(/[U^wa5. n12M5ˁ J(m9 `o87Kb#rs XƃT<8Z蘚5A•9>\H2Om/zьf@ gQ %X @7N{0'EƵTkg1hz4CLkW{9Ƈ-lɥ . dNM9"@ 6*Uhiuqߎ1u-+[ FB8Ff7 xQeh(vh+ҵ!q:~N`ήGNrAP$ng=! Y?l 8jp▰P|l,L;@ԧNK]XG `* %`L(/AN$o pN y AX=@sB2h;@yf4(D~A_Vςֻu>sȽ,d/]筅[TOό;?Ѐ8pD_'x Oсt?Ӂ @af# 8}]Wc0 ؀[J0Hp؁'JH *,؂?Z`0U 8pc <؃>(HBPF\JLX%8ħ(؂TXˠ XY /^^8b8dfxhj]vZr8tqxrӇ*8yxш8Xx"؉8XG؊8xJx؋8XxJʸ،8XH ؘڸ؍8不t蘎긎؎򨎱Tx؏5Yy ِi9Yyّ "9$Y&y(*#IY.0294Y6y8)<ٓ>@B9DYFŕJLٔNPRgUVyXZ\ٕ^`5dYfyhjlٖnir9tYvyxz|I)9YyYư٘9Yy ٙ9YٙVٚ9YyЛ9Yyș ^+h9Yyؙڹ V+0hYy虞깞ٞ9Yy̩TZYjP0 Z 0Z V i * $$(:!9%J$034 :,yo@&2:kPDڜ e }Ak0 @V jH*<:`N B:@fzhڡs[ڞУ R``*RZf*z|mzeУkPZrJN wjbЧ5p8ʠeK P p p:`zWЫ^ sЩ @pZ l`ֺ zj Jz蚮zЮ L ` ҚJj٪ʪs2:y* ۫[{ zp k026{86 ! ;ZZ )[&zeڰ0 R[:[9 Z# DHLO h V{жnpvZ^k`{tBJNki+q۸; w[Ӫ{[~k[g +{[ۺB['I۹P[ кۯu۲kK+؛{|2;:y /+[ֻ[۵ʋK۾;lamson-1.0pre11/doc/lamsonproject.org/output/index.html0000644000076500000240000002273311313464306022624 0ustar zedshawstaff LamsonProject: Lamson The Python Mail Server

Features

  • Avoid aliases forever! Lamson uses friendly regular expressions and FSM-based routing.

  • Use an RDBMS and ORM instead of a bizarre combination of flat-files and hashtables!

  • Get up and running quickly. Lamson can be installed and running in 30 seconds.

Get Started »

The Python SMTP Server

We've all been there, mucking around in the sendmail m4 macros trying one more time to get the damn mailing list to update for the new users. Every time we say, "This sucks, I want to rewrite this stupid thing." Yet, when we're done, we simply crawl back to our caves covered in our sendmail wounds.

Lamson's goal is to put an end to the hell that is "e-mail application development". Rather than stay stuck in the 1970s, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

Lamson The Python Mail Server

Rather than stay stuck in the 1970's, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

@route("(post_name)@(host)")
def START(message, post_name=None, host=None):
    message['X-Post-Name'] = post_name
    confirmation.send(relay, "post", message, "mail/confirm.msg", locals())
    return CONFIRMING

Instead of hideous aliases files (that you never remember to update) Lamson uses friendly regular expressions and routing decorators that make configuring how email is handled trivial.

@route("post-confirm-(id_number)@(host)", id_number="[a-z0-9]+")
def CONFIRMING(message, id_number=None, host=None):
    original = confirmation.verify(message, id_number)
    ...


@route("(post_name)@(host)")
@route("(post_name)-(action)@(host)", action="delete")
def POSTING(message, post_name=None, host=None, action=None):
    name, address = parseaddr(message['from'])
    ...

Rather than bizarre flat file "databases" and hashtable stores, Lamson uses anything that Python does to store data. You can use all of the following to store data in Lamson:

There's so many great ways to store data in Python that Lamson doesn't make any assumptions other than to provide a convenience function or two for configuring SQLAlchemy (since it's kind of a pain to setup). In reality, there's so many ORMs and storage mechanisms available to Python that you should try as many as you can and use the one you like the most.

Lamson also supports extensive spam blocking through SpamBayes:

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue", next_state=SPAMMING)
def START(message, **kw):
    ....

Bounce detection and analysis is available that regular people can use without going RFC nuts:

@route("(anything)@(host)", anything=".+", host=".+")
@bounce_to(soft=SOFT_BOUNCED, hard=HARD_BOUNCED)
def START(message, **kw):
    ...

All within a framework that works well with existing legacy mail servers so you can gradually get into using Lamson.

The 30 Second Introduction

If you have Python and easy_install already, then try this out:
$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc
$ lamson stop -ALL run

You now have a working base Lamson setup ready for you to work on with everything you need installed.

Next Steps

Grab the code and you can read through the quick start documentation. After you've gone through that, best thing to do is read the code to the examples/osb example included in the source distribution and read the rest of the documentation on this site.

At any point, you can get help for all available Lamson commands with:

$ lamson help

You can get individual command help with:

$ lamson help -for start

Finally, if you really want to get started using Lamson to implement your dream email application, but are completely lost, then you can contact me for help.


lamson-1.0pre11/doc/lamsonproject.org/output/index.txt0000644000076500000240000001072111227340344022470 0ustar zedshawstaffFrom: Zed Title: Lamson The Python Mail Server template: input/home_template.html Content-Type: text/html

Rather than stay stuck in the 1970's, Lamson adopts modern web application framework design and uses a proven scripting language (Python).

@route("(post_name)@(host)")
def START(message, post_name=None, host=None):
    message['X-Post-Name'] = post_name
    confirmation.send(relay, "post", message, "mail/confirm.msg", locals())
    return CONFIRMING

Instead of hideous aliases files (that you never remember to update) Lamson uses friendly regular expressions and routing decorators that make configuring how email is handled trivial.

@route("post-confirm-(id_number)@(host)", id_number="[a-z0-9]+")
def CONFIRMING(message, id_number=None, host=None):
    original = confirmation.verify(message, id_number)
    ...


@route("(post_name)@(host)")
@route("(post_name)-(action)@(host)", action="delete")
def POSTING(message, post_name=None, host=None, action=None):
    name, address = parseaddr(message['from'])
    ...

Rather than bizarre flat file "databases" and hashtable stores, Lamson uses anything that Python does to store data. You can use all of the following to store data in Lamson:

There's so many great ways to store data in Python that Lamson doesn't make any assumptions other than to provide a convenience function or two for configuring SQLAlchemy (since it's kind of a pain to setup). In reality, there's so many ORMs and storage mechanisms available to Python that you should try as many as you can and use the one you like the most.

Lamson also supports extensive spam blocking through SpamBayes:

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue", next_state=SPAMMING)
def START(message, **kw):
    ....

Bounce detection and analysis is available that regular people can use without going RFC nuts:

@route("(anything)@(host)", anything=".+", host=".+")
@bounce_to(soft=SOFT_BOUNCED, hard=HARD_BOUNCED)
def START(message, **kw):
    ...

All within a framework that works well with existing legacy mail servers so you can gradually get into using Lamson.

The 30 Second Introduction

If you have Python and easy_install already, then try this out:
$ easy_install lamson
$ lamson gen -project mymailserver
$ cd mymailserver
$ lamson start
$ lamson log
$ nosetests
$ lamson help -for send
$ lamson send -sender me@mydomain.com -to test@test.com \
        -subject "My test." -body "Hi there." -port 8823
$ less logs/lamson.log
$ mutt -F muttrc
$ lamson stop -ALL run

You now have a working base Lamson setup ready for you to work on with everything you need installed.

Next Steps

Grab the code and you can read through the quick start documentation. After you've gone through that, best thing to do is read the code to the examples/osb example included in the source distribution and read the rest of the documentation on this site.

At any point, you can get help for all available Lamson commands with:

$ lamson help

You can get individual command help with:

$ lamson help -for start

Finally, if you really want to get started using Lamson to implement your dream email application, but are completely lost, then you can contact me for help.

lamson-1.0pre11/doc/lamsonproject.org/output/lists/0000755000076500000240000000000011313464574021765 5ustar zedshawstafflamson-1.0pre11/doc/lamsonproject.org/output/lists/index.html0000644000076500000240000001277211313464306023764 0ustar zedshawstaff LamsonProject: Lamson Project Mailing Lists

Lamson Project Mailing Lists

Lamson’s mailing lists are hosted on librelist.com which is also written in Lamson.

You can subscribe to the list by sending an email to lamson@librelist.com with your first message. Remember that librelist posts this message, so introduce yourself.

If you want to read the archives for the Lamson list, you can browse them here in a very very basic form (soon to improve).

You can also get the archives for the Lamson list by using rsync:

$ rsync -azv librelist.com::archives/lamson lamson_archives

Be careful when you do this since that will get everything. It’s better to figure out which month or day you want using your web browser and then get that directory only.

IRC Channel

For people who don’t want to join the mailing list but still need help there’s a #lamson irc channel on irc.freenode.org. Come by and ask your question, and stick around since it might take people a little while to respond.


lamson-1.0pre11/doc/lamsonproject.org/output/lists/index.txt0000644000076500000240000000211111310754212023614 0ustar zedshawstaffFrom: Zed Title: Lamson Project Mailing Lists Lamson's mailing lists are hosted on "librelist.com":http://librelist.com/ which is also written in Lamson. You can subscribe to the list by sending an email to "lamson@librelist.com":mailto:lamson@librelist.com with your first message. *Remember that librelist posts this message, so introduce yourself.* If you want to read the archives for the Lamson list, you can browse them "here":http://librelist.com/browser/ in a very very basic form (soon to improve). You can also get the archives for the Lamson list by using rsync:
$ rsync -azv librelist.com::archives/lamson lamson_archives
Be careful when you do this since that will get everything. It's better to figure out which month or day you want using your web browser and then get that directory only. h2. IRC Channel For people who don't want to join the mailing list but still need help there's a #lamson irc channel on irc.freenode.org. Come by and ask your question, and stick around since it might take people a little while to respond. lamson-1.0pre11/doc/lamsonproject.org/output/mailocalypse.py0000644000076500000240000000534411215271227023661 0ustar zedshawstaffimport email from email.header import make_header, decode_header from string import capwords import sys import mailbox ALL_MAIL = 0 BAD_MAIL = 0 def all_parts(msg): parts = [m for m in msg.walk() if m != msg] if not parts: parts = [msg] return parts def collapse_header(header): if header.strip().startswith("=?"): decoded = decode_header(header) converted = (unicode( x[0], encoding=x[1] or 'ascii', errors='replace') for x in decoded) value = u"".join(converted) else: value = unicode(header, errors='replace') return value.encode("utf-8") def convert_header_insanity(header): if header is None: return header elif type(header) == list: return [collapse_header(h) for h in header] else: return collapse_header(header) def encode_header(name, val, charset='utf-8'): msg[name] = make_header([(val, charset)]).encode() def bless_headers(msg): # go through every header and convert it to utf-8 headers = {} for h in msg.keys(): headers[capwords(h, '-')] = convert_header_insanity(msg[h]) return headers def dump_headers(headers): for h in headers: print h, headers[h] def mail_load_cleanse(msg_file): global ALL_MAIL global BAD_MAIL msg = email.message_from_file(msg_file) headers = bless_headers(msg) # go through every body and convert it to utf-8 parts = all_parts(msg) bodies = [] for part in parts: guts = part.get_payload(decode=True) if part.get_content_maintype() == "text": charset = part.get_charsets()[0] try: if charset: uguts = unicode(guts, part.get_charsets()[0]) guts = uguts.encode("utf-8") else: guts = guts.encode("utf-8") except UnicodeDecodeError, exc: print >> sys.stderr, "CONFLICTED CHARSET:", exc, part.get_charsets() BAD_MAIL += 1 except LookupError, exc: print >> sys.stderr, "UNKNOWN CHARSET:", exc, part.get_charsets() BAD_MAIL += 1 except Exception, exc: print >> sys.stderr, "WEIRDO ERROR", exc, part.get_charsets() BAD_MAIL += 1 ALL_MAIL += 1 mb = None try: mb = mailbox.Maildir(sys.argv[1]) len(mb) # need this to make the maildir try to read the directory and fail except OSError: print "NOT A MAILDIR, TRYING MBOX" mb = mailbox.mbox(sys.argv[1]) if not mb: print "NOT A MAILDIR OR MBOX, SORRY" for key in mb.keys(): mail_load_cleanse(mb.get_file(key)) print >> sys.stderr, "ALL", ALL_MAIL print >> sys.stderr, "BAD", BAD_MAIL lamson-1.0pre11/doc/lamsonproject.org/output/prettify.css0000644000076500000240000000134611213057477023212 0ustar zedshawstaff/* Pretty printing styles. Used with prettify.js. */ .str { color: #ed9d13; } .kwd { color: #6ab825; } .com { color: #999999; font-style: italic; } .typ { color: #447fcf; } .lit { color: #cd2828; } .pun { color: #e2e2e2; } .pln { color: #f2f2f2; } .tag { color: #cd2828; } .atn { color: #f2f2f2; } .atv { color: #f2f2f2; } .dec { color: #f2f2f2; } pre.prettyprint { padding: 2px; border: 1px solid #888; } @media print { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun { color: #440; } .pln { color: #000; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } lamson-1.0pre11/doc/lamsonproject.org/output/._prettify.js0000644000076500000240000000031111213050674023232 0ustar zedshawstaffMac OS X  2ATTRH311com.apple.quarantineq/0000;4a2c5065;Firefox.app;|org.mozilla.firefoxlamson-1.0pre11/doc/lamsonproject.org/output/prettify.js0000644000076500000240000015324711213050674023036 0ustar zedshawstaff// Copyright (C) 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview * some functions for browser-side pretty printing of code contained in html. * * The lexer should work on a number of languages including C and friends, * Java, Python, Bash, SQL, HTML, XML, CSS, Javascript, and Makefiles. * It works passably on Ruby, PHP and Awk and a decent subset of Perl, but, * because of commenting conventions, doesn't work on Smalltalk, Lisp-like, or * CAML-like languages. * * If there's a language not mentioned here, then I don't know it, and don't * know whether it works. If it has a C-like, Bash-like, or XML-like syntax * then it should work passably. * * Usage: * 1) include this source file in an html page via * * 2) define style rules. See the example page for examples. * 3) mark the
 and  tags in your source with class=prettyprint.
 *    You can also use the (html deprecated)  tag, but the pretty printer
 *    needs to do more substantial DOM manipulations to support that, so some
 *    css styles may not be preserved.
 * That's it.  I wanted to keep the API as simple as possible, so there's no
 * need to specify which language the code is in.
 *
 * Change log:
 * cbeust, 2006/08/22
 *   Java annotations (start with "@") are now captured as literals ("lit")
 */

// JSLint declarations
/*global console, document, navigator, setTimeout, window */

/**
 * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
 * UI events.
 * If set to {@code false}, {@code prettyPrint()} is synchronous.
 */
window['PR_SHOULD_USE_CONTINUATION'] = true;

/** the number of characters between tab columns */
window['PR_TAB_WIDTH'] = 8;

/** Walks the DOM returning a properly escaped version of innerHTML.
  * @param {Node} node
  * @param {Array.<string>} out output buffer that receives chunks of HTML.
  */
window['PR_normalizedHtml']

/** Contains functions for creating and registering new language handlers.
  * @type {Object}
  */
  = window['PR']

/** Pretty print a chunk of code.
  *
  * @param {string} sourceCodeHtml code as html
  * @return {string} code as html, but prettier
  */
  = window['prettyPrintOne']
/** Find all the {@code <pre>} and {@code <code>} tags in the DOM with
  * {@code class=prettyprint} and prettify them.
  * @param {Function?} opt_whenDone if specified, called when the last entry
  *     has been finished.
  */
  = window['prettyPrint'] = void 0;

/** browser detection. @extern */
window['_pr_isIE6'] = function () {
  var isIE6 = navigator && navigator.userAgent &&
      /\bMSIE 6\./.test(navigator.userAgent);
  window['_pr_isIE6'] = function () { return isIE6; };
  return isIE6;
};


(function () {
  // Keyword lists for various languages.
  var FLOW_CONTROL_KEYWORDS =
      "break continue do else for if return while ";
  var C_KEYWORDS = FLOW_CONTROL_KEYWORDS + "auto case char const default " +
      "double enum extern float goto int long register short signed sizeof " +
      "static struct switch typedef union unsigned void volatile ";
  var COMMON_KEYWORDS = C_KEYWORDS + "catch class delete false import " +
      "new operator private protected public this throw true try ";
  var CPP_KEYWORDS = COMMON_KEYWORDS + "alignof align_union asm axiom bool " +
      "concept concept_map const_cast constexpr decltype " +
      "dynamic_cast explicit export friend inline late_check " +
      "mutable namespace nullptr reinterpret_cast static_assert static_cast " +
      "template typeid typename typeof using virtual wchar_t where ";
  var JAVA_KEYWORDS = COMMON_KEYWORDS +
      "boolean byte extends final finally implements import instanceof null " +
      "native package strictfp super synchronized throws transient ";
  var CSHARP_KEYWORDS = JAVA_KEYWORDS +
      "as base by checked decimal delegate descending event " +
      "fixed foreach from group implicit in interface internal into is lock " +
      "object out override orderby params partial readonly ref sbyte sealed " +
      "stackalloc string select uint ulong unchecked unsafe ushort var ";
  var JSCRIPT_KEYWORDS = COMMON_KEYWORDS +
      "debugger eval export function get null set undefined var with " +
      "Infinity NaN ";
  var PERL_KEYWORDS = "caller delete die do dump elsif eval exit foreach for " +
      "goto if import last local my next no our print package redo require " +
      "sub undef unless until use wantarray while BEGIN END ";
  var PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + "and as assert class def del " +
      "elif except exec finally from global import in is lambda " +
      "nonlocal not or pass print raise try with yield " +
      "False True None ";
  var RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + "alias and begin case class def" +
      " defined elsif end ensure false in module next nil not or redo rescue " +
      "retry self super then true undef unless until when yield BEGIN END ";
  var SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + "case done elif esac eval fi " +
      "function in local set then until ";
  var ALL_KEYWORDS = (
      CPP_KEYWORDS + CSHARP_KEYWORDS + JSCRIPT_KEYWORDS + PERL_KEYWORDS +
      PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS);

  // token style names.  correspond to css classes
  /** token style for a string literal */
  var PR_STRING = 'str';
  /** token style for a keyword */
  var PR_KEYWORD = 'kwd';
  /** token style for a comment */
  var PR_COMMENT = 'com';
  /** token style for a type */
  var PR_TYPE = 'typ';
  /** token style for a literal value.  e.g. 1, null, true. */
  var PR_LITERAL = 'lit';
  /** token style for a punctuation string. */
  var PR_PUNCTUATION = 'pun';
  /** token style for a punctuation string. */
  var PR_PLAIN = 'pln';

  /** token style for an sgml tag. */
  var PR_TAG = 'tag';
  /** token style for a markup declaration such as a DOCTYPE. */
  var PR_DECLARATION = 'dec';
  /** token style for embedded source. */
  var PR_SOURCE = 'src';
  /** token style for an sgml attribute name. */
  var PR_ATTRIB_NAME = 'atn';
  /** token style for an sgml attribute value. */
  var PR_ATTRIB_VALUE = 'atv';

  /**
   * A class that indicates a section of markup that is not code, e.g. to allow
   * embedding of line numbers within code listings.
   */
  var PR_NOCODE = 'nocode';

  /** A set of tokens that can precede a regular expression literal in
    * javascript.
    * http://www.mozilla.org/js/language/js20/rationale/syntax.html has the full
    * list, but I've removed ones that might be problematic when seen in
    * languages that don't support regular expression literals.
    *
    * <p>Specifically, I've removed any keywords that can't precede a regexp
    * literal in a syntactically legal javascript program, and I've removed the
    * "in" keyword since it's not a keyword in many languages, and might be used
    * as a count of inches.
    *
    * <p>The link a above does not accurately describe EcmaScript rules since
    * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
    * very well in practice.
    *
    * @private
    */
  var REGEXP_PRECEDER_PATTERN = function () {
      var preceders = [
          "!", "!=", "!==", "#", "%", "%=", "&", "&&", "&&=",
          "&=", "(", "*", "*=", /* "+", */ "+=", ",", /* "-", */ "-=",
          "->", /*".", "..", "...", handled below */ "/", "/=", ":", "::", ";",
          "<", "<<", "<<=", "<=", "=", "==", "===", ">",
          ">=", ">>", ">>=", ">>>", ">>>=", "?", "@", "[",
          "^", "^=", "^^", "^^=", "{", "|", "|=", "||",
          "||=", "~" /* handles =~ and !~ */,
          "break", "case", "continue", "delete",
          "do", "else", "finally", "instanceof",
          "return", "throw", "try", "typeof"
          ];
      var pattern = '(?:^^|[+-]';
      for (var i = 0; i < preceders.length; ++i) {
        pattern += '|' + preceders[i].replace(/([^=<>:&a-z])/g, '\\$1');
      }
      pattern += ')\\s*';  // matches at end, and matches empty string
      return pattern;
      // CAVEAT: this does not properly handle the case where a regular
      // expression immediately follows another since a regular expression may
      // have flags for case-sensitivity and the like.  Having regexp tokens
      // adjacent is not valid in any language I'm aware of, so I'm punting.
      // TODO: maybe style special characters inside a regexp as punctuation.
    }();

  // Define regexps here so that the interpreter doesn't have to create an
  // object each time the function containing them is called.
  // The language spec requires a new object created even if you don't access
  // the $1 members.
  var pr_amp = /&/g;
  var pr_lt = /</g;
  var pr_gt = />/g;
  var pr_quot = /\"/g;
  /** like textToHtml but escapes double quotes to be attribute safe. */
  function attribToHtml(str) {
    return str.replace(pr_amp, '&amp;')
        .replace(pr_lt, '&lt;')
        .replace(pr_gt, '&gt;')
        .replace(pr_quot, '&quot;');
  }

  /** escapest html special characters to html. */
  function textToHtml(str) {
    return str.replace(pr_amp, '&amp;')
        .replace(pr_lt, '&lt;')
        .replace(pr_gt, '&gt;');
  }


  var pr_ltEnt = /&lt;/g;
  var pr_gtEnt = /&gt;/g;
  var pr_aposEnt = /&apos;/g;
  var pr_quotEnt = /&quot;/g;
  var pr_ampEnt = /&amp;/g;
  var pr_nbspEnt = /&nbsp;/g;
  /** unescapes html to plain text. */
  function htmlToText(html) {
    var pos = html.indexOf('&');
    if (pos < 0) { return html; }
    // Handle numeric entities specially.  We can't use functional substitution
    // since that doesn't work in older versions of Safari.
    // These should be rare since most browsers convert them to normal chars.
    for (--pos; (pos = html.indexOf('&#', pos + 1)) >= 0;) {
      var end = html.indexOf(';', pos);
      if (end >= 0) {
        var num = html.substring(pos + 3, end);
        var radix = 10;
        if (num && num.charAt(0) === 'x') {
          num = num.substring(1);
          radix = 16;
        }
        var codePoint = parseInt(num, radix);
        if (!isNaN(codePoint)) {
          html = (html.substring(0, pos) + String.fromCharCode(codePoint) +
                  html.substring(end + 1));
        }
      }
    }

    return html.replace(pr_ltEnt, '<')
        .replace(pr_gtEnt, '>')
        .replace(pr_aposEnt, "'")
        .replace(pr_quotEnt, '"')
        .replace(pr_ampEnt, '&')
        .replace(pr_nbspEnt, ' ');
  }

  /** is the given node's innerHTML normally unescaped? */
  function isRawContent(node) {
    return 'XMP' === node.tagName;
  }

  function normalizedHtml(node, out) {
    switch (node.nodeType) {
      case 1:  // an element
        var name = node.tagName.toLowerCase();
        out.push('<', name);
        for (var i = 0; i < node.attributes.length; ++i) {
          var attr = node.attributes[i];
          if (!attr.specified) { continue; }
          out.push(' ');
          normalizedHtml(attr, out);
        }
        out.push('>');
        for (var child = node.firstChild; child; child = child.nextSibling) {
          normalizedHtml(child, out);
        }
        if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
          out.push('<\/', name, '>');
        }
        break;
      case 2: // an attribute
        out.push(node.name.toLowerCase(), '="', attribToHtml(node.value), '"');
        break;
      case 3: case 4: // text
        out.push(textToHtml(node.nodeValue));
        break;
    }
  }

  /**
   * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
   * matches the union o the sets o strings matched d by the input RegExp.
   * Since it matches globally, if the input strings have a start-of-input
   * anchor (/^.../), it is ignored for the purposes of unioning.
   * @param {Array.<RegExpr>} regexs non multiline, non-global regexs.
   * @return {RegExp} a global regex.
   */
  function combinePrefixPatterns(regexs) {
    var capturedGroupIndex = 0;

    var needToFoldCase = false;
    var ignoreCase = false;
    for (var i = 0, n = regexs.length; i < n; ++i) {
      var regex = regexs[i];
      if (regex.ignoreCase) {
        ignoreCase = true;
      } else if (/[a-z]/i.test(regex.source.replace(
                     /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
        needToFoldCase = true;
        ignoreCase = false;
        break;
      }
    }

    function decodeEscape(charsetPart) {
      if (charsetPart.charAt(0) !== '\\') { return charsetPart.charCodeAt(0); }
      switch (charsetPart.charAt(1)) {
        case 'b': return 8;
        case 't': return 9;
        case 'n': return 0xa;
        case 'v': return 0xb;
        case 'f': return 0xc;
        case 'r': return 0xd;
        case 'u': case 'x':
          return parseInt(charsetPart.substring(2), 16)
              || charsetPart.charCodeAt(1);
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7':
          return parseInt(charsetPart.substring(1), 8);
        default: return charsetPart.charCodeAt(1);
      }
    }

    function encodeEscape(charCode) {
      if (charCode < 0x20) {
        return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
      }
      var ch = String.fromCharCode(charCode);
      if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
        ch = '\\' + ch;
      }
      return ch;
    }

    function caseFoldCharset(charSet) {
      var charsetParts = charSet.substring(1, charSet.length - 1).match(
          new RegExp(
              '\\\\u[0-9A-Fa-f]{4}'
              + '|\\\\x[0-9A-Fa-f]{2}'
              + '|\\\\[0-3][0-7]{0,2}'
              + '|\\\\[0-7]{1,2}'
              + '|\\\\[\\s\\S]'
              + '|-'
              + '|[^-\\\\]',
              'g'));
      var groups = [];
      var ranges = [];
      var inverse = charsetParts[0] === '^';
      for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
        var p = charsetParts[i];
        switch (p) {
          case '\\B': case '\\b':
          case '\\D': case '\\d':
          case '\\S': case '\\s':
          case '\\W': case '\\w':
            groups.push(p);
            continue;
        }
        var start = decodeEscape(p);
        var end;
        if (i + 2 < n && '-' === charsetParts[i + 1]) {
          end = decodeEscape(charsetParts[i + 2]);
          i += 2;
        } else {
          end = start;
        }
        ranges.push([start, end]);
        // If the range might intersect letters, then expand it.
        if (!(end < 65 || start > 122)) {
          if (!(end < 65 || start > 90)) {
            ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
          }
          if (!(end < 97 || start > 122)) {
            ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
          }
        }
      }

      // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
      // -> [[1, 12], [14, 14], [16, 17]]
      ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1]  - a[1]); });
      var consolidatedRanges = [];
      var lastRange = [NaN, NaN];
      for (var i = 0; i < ranges.length; ++i) {
        var range = ranges[i];
        if (range[0] <= lastRange[1] + 1) {
          lastRange[1] = Math.max(lastRange[1], range[1]);
        } else {
          consolidatedRanges.push(lastRange = range);
        }
      }

      var out = ['['];
      if (inverse) { out.push('^'); }
      out.push.apply(out, groups);
      for (var i = 0; i < consolidatedRanges.length; ++i) {
        var range = consolidatedRanges[i];
        out.push(encodeEscape(range[0]));
        if (range[1] > range[0]) {
          if (range[1] + 1 > range[0]) { out.push('-'); }
          out.push(encodeEscape(range[1]));
        }
      }
      out.push(']');
      return out.join('');
    }

    function allowAnywhereFoldCaseAndRenumberGroups(regex) {
      // Split into character sets, escape sequences, punctuation strings
      // like ('(', '(?:', ')', '^'), and runs of characters that do not
      // include any of the above.
      var parts = regex.source.match(
          new RegExp(
              '(?:'
              + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]'  // a character set
              + '|\\\\u[A-Fa-f0-9]{4}'  // a unicode escape
              + '|\\\\x[A-Fa-f0-9]{2}'  // a hex escape
              + '|\\\\[0-9]+'  // a back-reference or octal escape
              + '|\\\\[^ux0-9]'  // other escape sequence
              + '|\\(\\?[:!=]'  // start of a non-capturing group
              + '|[\\(\\)\\^]'  // start/emd of a group, or line start
              + '|[^\\x5B\\x5C\\(\\)\\^]+'  // run of other characters
              + ')',
              'g'));
      var n = parts.length;

      // Maps captured group numbers to the number they will occupy in
      // the output or to -1 if that has not been determined, or to
      // undefined if they need not be capturing in the output.
      var capturedGroups = [];

      // Walk over and identify back references to build the capturedGroups
      // mapping.
      var groupIndex;
      for (var i = 0, groupIndex = 0; i < n; ++i) {
        var p = parts[i];
        if (p === '(') {
          // groups are 1-indexed, so max group index is count of '('
          ++groupIndex;
        } else if ('\\' === p.charAt(0)) {
          var decimalValue = +p.substring(1);
          if (decimalValue && decimalValue <= groupIndex) {
            capturedGroups[decimalValue] = -1;
          }
        }
      }

      // Renumber groups and reduce capturing groups to non-capturing groups
      // where possible.
      for (var i = 1; i < capturedGroups.length; ++i) {
        if (-1 === capturedGroups[i]) {
          capturedGroups[i] = ++capturedGroupIndex;
        }
      }
      for (var i = 0, groupIndex = 0; i < n; ++i) {
        var p = parts[i];
        if (p === '(') {
          ++groupIndex;
          if (capturedGroups[groupIndex] === undefined) {
            parts[i] = '(?:';
          }
        } else if ('\\' === p.charAt(0)) {
          var decimalValue = +p.substring(1);
          if (decimalValue && decimalValue <= groupIndex) {
            parts[i] = '\\' + capturedGroups[groupIndex];
          }
        }
      }

      // Remove any prefix anchors so that the output will match anywhere.
      // ^^ really does mean an anchored match though.
      for (var i = 0, groupIndex = 0; i < n; ++i) {
        if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
      }

      // Expand letters to groupts to handle mixing of case-sensitive and
      // case-insensitive patterns if necessary.
      if (regex.ignoreCase && needToFoldCase) {
        for (var i = 0; i < n; ++i) {
          var p = parts[i];
          var ch0 = p.charAt(0);
          if (p.length >= 2 && ch0 === '[') {
            parts[i] = caseFoldCharset(p);
          } else if (ch0 !== '\\') {
            // TODO: handle letters in numeric escapes.
            parts[i] = p.replace(
                /[a-zA-Z]/g,
                function (ch) {
                  var cc = ch.charCodeAt(0);
                  return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
                });
          }
        }
      }

      return parts.join('');
    }

    var rewritten = [];
    for (var i = 0, n = regexs.length; i < n; ++i) {
      var regex = regexs[i];
      if (regex.global || regex.multiline) { throw new Error('' + regex); }
      rewritten.push(
          '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
    }

    return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
  }

  var PR_innerHtmlWorks = null;
  function getInnerHtml(node) {
    // inner html is hopelessly broken in Safari 2.0.4 when the content is
    // an html description of well formed XML and the containing tag is a PRE
    // tag, so we detect that case and emulate innerHTML.
    if (null === PR_innerHtmlWorks) {
      var testNode = document.createElement('PRE');
      testNode.appendChild(
          document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));
      PR_innerHtmlWorks = !/</.test(testNode.innerHTML);
    }

    if (PR_innerHtmlWorks) {
      var content = node.innerHTML;
      // XMP tags contain unescaped entities so require special handling.
      if (isRawContent(node)) {
        content = textToHtml(content);
      }
      return content;
    }

    var out = [];
    for (var child = node.firstChild; child; child = child.nextSibling) {
      normalizedHtml(child, out);
    }
    return out.join('');
  }

  /** returns a function that expand tabs to spaces.  This function can be fed
    * successive chunks of text, and will maintain its own internal state to
    * keep track of how tabs are expanded.
    * @return {function (string) : string} a function that takes
    *   plain text and return the text with tabs expanded.
    * @private
    */
  function makeTabExpander(tabWidth) {
    var SPACES = '                ';
    var charInLine = 0;

    return function (plainText) {
      // walk over each character looking for tabs and newlines.
      // On tabs, expand them.  On newlines, reset charInLine.
      // Otherwise increment charInLine
      var out = null;
      var pos = 0;
      for (var i = 0, n = plainText.length; i < n; ++i) {
        var ch = plainText.charAt(i);

        switch (ch) {
          case '\t':
            if (!out) { out = []; }
            out.push(plainText.substring(pos, i));
            // calculate how much space we need in front of this part
            // nSpaces is the amount of padding -- the number of spaces needed
            // to move us to the next column, where columns occur at factors of
            // tabWidth.
            var nSpaces = tabWidth - (charInLine % tabWidth);
            charInLine += nSpaces;
            for (; nSpaces >= 0; nSpaces -= SPACES.length) {
              out.push(SPACES.substring(0, nSpaces));
            }
            pos = i + 1;
            break;
          case '\n':
            charInLine = 0;
            break;
          default:
            ++charInLine;
        }
      }
      if (!out) { return plainText; }
      out.push(plainText.substring(pos));
      return out.join('');
    };
  }

  var pr_chunkPattern = new RegExp(
      '[^<]+'  // A run of characters other than '<'
      + '|<\!--[\\s\\S]*?--\>'  // an HTML comment
      + '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>'  // a CDATA section
      + '|</?[a-zA-Z][^>]*>'  // a probable tag that should not be highlighted
      + '|<',  // A '<' that does not begin a larger chunk
      'g');
  var pr_commentPrefix = /^<\!--/;
  var pr_cdataPrefix = /^<\[CDATA\[/;
  var pr_brPrefix = /^<br\b/i;
  var pr_tagNameRe = /^<(\/?)([a-zA-Z]+)/;

  /** split markup into chunks of html tags (style null) and
    * plain text (style {@link #PR_PLAIN}), converting tags which are
    * significant for tokenization (<br>) into their textual equivalent.
    *
    * @param {string} s html where whitespace is considered significant.
    * @return {Object} source code and extracted tags.
    * @private
    */
  function extractTags(s) {
    // since the pattern has the 'g' modifier and defines no capturing groups,
    // this will return a list of all chunks which we then classify and wrap as
    // PR_Tokens
    var matches = s.match(pr_chunkPattern);
    var sourceBuf = [];
    var sourceBufLen = 0;
    var extractedTags = [];
    if (matches) {
      for (var i = 0, n = matches.length; i < n; ++i) {
        var match = matches[i];
        if (match.length > 1 && match.charAt(0) === '<') {
          if (pr_commentPrefix.test(match)) { continue; }
          if (pr_cdataPrefix.test(match)) {
            // strip CDATA prefix and suffix.  Don't unescape since it's CDATA
            sourceBuf.push(match.substring(9, match.length - 3));
            sourceBufLen += match.length - 12;
          } else if (pr_brPrefix.test(match)) {
            // <br> tags are lexically significant so convert them to text.
            // This is undone later.
            sourceBuf.push('\n');
            ++sourceBufLen;
          } else {
            if (match.indexOf(PR_NOCODE) >= 0 && isNoCodeTag(match)) {
              // A <span class="nocode"> will start a section that should be
              // ignored.  Continue walking the list until we see a matching end
              // tag.
              var name = match.match(pr_tagNameRe)[2];
              var depth = 1;
              var j;
              end_tag_loop:
              for (j = i + 1; j < n; ++j) {
                var name2 = matches[j].match(pr_tagNameRe);
                if (name2 && name2[2] === name) {
                  if (name2[1] === '/') {
                    if (--depth === 0) { break end_tag_loop; }
                  } else {
                    ++depth;
                  }
                }
              }
              if (j < n) {
                extractedTags.push(
                    sourceBufLen, matches.slice(i, j + 1).join(''));
                i = j;
              } else {  // Ignore unclosed sections.
                extractedTags.push(sourceBufLen, match);
              }
            } else {
              extractedTags.push(sourceBufLen, match);
            }
          }
        } else {
          var literalText = htmlToText(match);
          sourceBuf.push(literalText);
          sourceBufLen += literalText.length;
        }
      }
    }
    return { source: sourceBuf.join(''), tags: extractedTags };
  }

  /** True if the given tag contains a class attribute with the nocode class. */
  function isNoCodeTag(tag) {
    return !!tag
        // First canonicalize the representation of attributes
        .replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,
                 ' $1="$2$3$4"')
        // Then look for the attribute we want.
        .match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/);
  }

  /**
   * Apply the given language handler to sourceCode and add the resulting
   * decorations to out.
   * @param {number} basePos the index of sourceCode within the chunk of source
   *    whose decorations are already present on out.
   */
  function appendDecorations(basePos, sourceCode, langHandler, out) {
    if (!sourceCode) { return; }
    var job = {
      source: sourceCode,
      basePos: basePos
    };
    langHandler(job);
    out.push.apply(out, job.decorations);
  }

  /** Given triples of [style, pattern, context] returns a lexing function,
    * The lexing function interprets the patterns to find token boundaries and
    * returns a decoration list of the form
    * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
    * where index_n is an index into the sourceCode, and style_n is a style
    * constant like PR_PLAIN.  index_n-1 <= index_n, and style_n-1 applies to
    * all characters in sourceCode[index_n-1:index_n].
    *
    * The stylePatterns is a list whose elements have the form
    * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
    *
    * Style is a style constant like PR_PLAIN, or can be a string of the
    * form 'lang-FOO', where FOO is a language extension describing the
    * language of the portion of the token in $1 after pattern executes.
    * E.g., if style is 'lang-lisp', and group 1 contains the text
    * '(hello (world))', then that portion of the token will be passed to the
    * registered lisp handler for formatting.
    * The text before and after group 1 will be restyled using this decorator
    * so decorators should take care that this doesn't result in infinite
    * recursion.  For example, the HTML lexer rule for SCRIPT elements looks
    * something like ['lang-js', /<[s]cript>(.+?)<\/script>/].  This may match
    * '<script>foo()<\/script>', which would cause the current decorator to
    * be called with '<script>' which would not match the same rule since
    * group 1 must not be empty, so it would be instead styled as PR_TAG by
    * the generic tag rule.  The handler registered for the 'js' extension would
    * then be called with 'foo()', and finally, the current decorator would
    * be called with '<\/script>' which would not match the original rule and
    * so the generic tag rule would identify it as a tag.
    *
    * Pattern must only match prefixes, and if it matches a prefix, then that
    * match is considered a token with the same style.
    *
    * Context is applied to the last non-whitespace, non-comment token
    * recognized.
    *
    * Shortcut is an optional string of characters, any of which, if the first
    * character, gurantee that this pattern and only this pattern matches.
    *
    * @param {Array} shortcutStylePatterns patterns that always start with
    *   a known character.  Must have a shortcut string.
    * @param {Array} fallthroughStylePatterns patterns that will be tried in
    *   order if the shortcut ones fail.  May have shortcuts.
    *
    * @return {function (Object)} a
    *   function that takes source code and returns a list of decorations.
    */
  function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
    var shortcuts = {};
    var tokenizer;
    (function () {
      var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
      var allRegexs = [];
      var regexKeys = {};
      for (var i = 0, n = allPatterns.length; i < n; ++i) {
        var patternParts = allPatterns[i];
        var shortcutChars = patternParts[3];
        if (shortcutChars) {
          for (var c = shortcutChars.length; --c >= 0;) {
            shortcuts[shortcutChars.charAt(c)] = patternParts;
          }
        }
        var regex = patternParts[1];
        var k = '' + regex;
        if (!regexKeys.hasOwnProperty(k)) {
          allRegexs.push(regex);
          regexKeys[k] = null;
        }
      }
      allRegexs.push(/[\0-\uffff]/);
      tokenizer = combinePrefixPatterns(allRegexs);
    })();

    var nPatterns = fallthroughStylePatterns.length;
    var notWs = /\S/;

    /**
     * Lexes job.source and produces an output array job.decorations of style
     * classes preceded by the position at which they start in job.source in
     * order.
     *
     * @param {Object} job an object like {@code
     *    source: {string} sourceText plain text,
     *    basePos: {int} position of job.source in the larger chunk of
     *        sourceCode.
     * }
     */
    var decorate = function (job) {
      var sourceCode = job.source, basePos = job.basePos;
      /** Even entries are positions in source in ascending order.  Odd enties
        * are style markers (e.g., PR_COMMENT) that run from that position until
        * the end.
        * @type {Array.<number|string>}
        */
      var decorations = [basePos, PR_PLAIN];
      var pos = 0;  // index into sourceCode
      var tokens = sourceCode.match(tokenizer) || [];
      var styleCache = {};

      for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
        var token = tokens[ti];
        var style = styleCache[token];
        var match;

        var isEmbedded;
        if (typeof style === 'string') {
          isEmbedded = false;
        } else {
          var patternParts = shortcuts[token.charAt(0)];
          if (patternParts) {
            match = token.match(patternParts[1]);
            style = patternParts[0];
          } else {
            for (var i = 0; i < nPatterns; ++i) {
              patternParts = fallthroughStylePatterns[i];
              match = token.match(patternParts[1]);
              if (match) {
                style = patternParts[0];
                break;
              }
            }

            if (!match) {  // make sure that we make progress
              style = PR_PLAIN;
            }
          }

          isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
          if (isEmbedded && !(match && match[1])) {
            isEmbedded = false;
            style = PR_SOURCE;
          }

          if (!isEmbedded) { styleCache[token] = style; }
        }

        var tokenStart = pos;
        pos += token.length;

        if (!isEmbedded) {
          decorations.push(basePos + tokenStart, style);
        } else {  // Treat group 1 as an embedded block of source code.
          var embeddedSource = match[1];
          var embeddedSourceStart = token.indexOf(embeddedSource);
          var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
          var lang = style.substring(5);
          var size = decorations.length - 10;
          // Decorate the left of the embedded source
          appendDecorations(
              basePos + tokenStart,
              token.substring(0, embeddedSourceStart),
              decorate, decorations);
          // Decorate the embedded source
          appendDecorations(
              basePos + tokenStart + embeddedSourceStart,
              embeddedSource,
              langHandlerForExtension(lang, embeddedSource),
              decorations);
          // Decorate the right of the embedded section
          appendDecorations(
              basePos + tokenStart + embeddedSourceEnd,
              token.substring(embeddedSourceEnd),
              decorate, decorations);
        }
      }
      job.decorations = decorations;
    };
    return decorate;
  }

  /** returns a function that produces a list of decorations from source text.
    *
    * This code treats ", ', and ` as string delimiters, and \ as a string
    * escape.  It does not recognize perl's qq() style strings.
    * It has no special handling for double delimiter escapes as in basic, or
    * the tripled delimiters used in python, but should work on those regardless
    * although in those cases a single string literal may be broken up into
    * multiple adjacent string literals.
    *
    * It recognizes C, C++, and shell style comments.
    *
    * @param {Object} options a set of optional parameters.
    * @return {function (Object)} a function that examines the source code
    *     in the input job and builds the decoration list.
    */
  function sourceDecorator(options) {
    var shortcutStylePatterns = [], fallthroughStylePatterns = [];
    if (options['tripleQuotedStrings']) {
      // '''multi-line-string''', 'single-line-string', and double-quoted
      shortcutStylePatterns.push(
          [PR_STRING,  /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
           null, '\'"']);
    } else if (options['multiLineStrings']) {
      // 'multi-line-string', "multi-line-string"
      shortcutStylePatterns.push(
          [PR_STRING,  /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
           null, '\'"`']);
    } else {
      // 'single-line-string', "single-line-string"
      shortcutStylePatterns.push(
          [PR_STRING,
           /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
           null, '"\'']);
    }
    if (options['hashComments']) {
      if (options['cStyleComments']) {
        // Stop C preprocessor declarations at an unclosed open comment
        shortcutStylePatterns.push(
            [PR_COMMENT, /^#(?:[^\r\n\/]|\/(?!\*)|\/\*[^\r\n]*?\*\/)*/,
             null, '#']);
      } else {
        shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
      }
    }
    if (options['cStyleComments']) {
      fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
      fallthroughStylePatterns.push(
          [PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
    }
    if (options['regexLiterals']) {
      var REGEX_LITERAL = (
          // A regular expression literal starts with a slash that is
          // not followed by * or / so that it is not confused with
          // comments.
          '/(?=[^/*])'
          // and then contains any number of raw characters,
          + '(?:[^/\\x5B\\x5C]'
          // escape sequences (\x5C),
          +    '|\\x5C[\\s\\S]'
          // or non-nesting character sets (\x5B\x5D);
          +    '|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+'
          // finally closed by a /.
          + '/');
      fallthroughStylePatterns.push(
          ['lang-regex',
           new RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
           ]);
    }

    var keywords = options['keywords'].replace(/^\s+|\s+$/g, '');
    if (keywords.length) {
      fallthroughStylePatterns.push(
          [PR_KEYWORD,
           new RegExp('^(?:' + keywords.replace(/\s+/g, '|') + ')\\b'), null]);
    }

    shortcutStylePatterns.push([PR_PLAIN,       /^\s+/, null, ' \r\n\t\xA0']);
    fallthroughStylePatterns.push(
        // TODO(mikesamuel): recognize non-latin letters and numerals in idents
        [PR_LITERAL,     /^@[a-z_$][a-z_$@0-9]*/i, null, '@'],
        [PR_TYPE,        /^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, null],
        [PR_PLAIN,       /^[a-z_$][a-z_$@0-9]*/i, null],
        [PR_LITERAL,
         new RegExp(
             '^(?:'
             // A hex number
             + '0x[a-f0-9]+'
             // or an octal or decimal number,
             + '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
             // possibly in scientific notation
             + '(?:e[+\\-]?\\d+)?'
             + ')'
             // with an optional modifier like UL for unsigned long
             + '[a-z]*', 'i'),
         null, '0123456789'],
        [PR_PUNCTUATION, /^.[^\s\w\.$@\'\"\`\/\#]*/, null]);

    return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
  }

  var decorateSource = sourceDecorator({
        'keywords': ALL_KEYWORDS,
        'hashComments': true,
        'cStyleComments': true,
        'multiLineStrings': true,
        'regexLiterals': true
      });

  /** Breaks {@code job.source} around style boundaries in
    * {@code job.decorations} while re-interleaving {@code job.extractedTags},
    * and leaves the result in {@code job.prettyPrintedHtml}.
    * @param {Object} job like {
    *    source: {string} source as plain text,
    *    extractedTags: {Array.<number|string>} extractedTags chunks of raw
    *                   html preceded by their position in {@code job.source}
    *                   in order
    *    decorations: {Array.<number|string} an array of style classes preceded
    *                 by the position at which they start in job.source in order
    * }
    * @private
    */
  function recombineTagsAndDecorations(job) {
    var sourceText = job.source;
    var extractedTags = job.extractedTags;
    var decorations = job.decorations;

    var html = [];
    // index past the last char in sourceText written to html
    var outputIdx = 0;

    var openDecoration = null;
    var currentDecoration = null;
    var tagPos = 0;  // index into extractedTags
    var decPos = 0;  // index into decorations
    var tabExpander = makeTabExpander(window['PR_TAB_WIDTH']);

    var adjacentSpaceRe = /([\r\n ]) /g;
    var startOrSpaceRe = /(^| ) /gm;
    var newlineRe = /\r\n?|\n/g;
    var trailingSpaceRe = /[ \r\n]$/;
    var lastWasSpace = true;  // the last text chunk emitted ended with a space.

    // A helper function that is responsible for opening sections of decoration
    // and outputing properly escaped chunks of source
    function emitTextUpTo(sourceIdx) {
      if (sourceIdx > outputIdx) {
        if (openDecoration && openDecoration !== currentDecoration) {
          // Close the current decoration
          html.push('</span>');
          openDecoration = null;
        }
        if (!openDecoration && currentDecoration) {
          openDecoration = currentDecoration;
          html.push('<span class="', openDecoration, '">');
        }
        // This interacts badly with some wikis which introduces paragraph tags
        // into pre blocks for some strange reason.
        // It's necessary for IE though which seems to lose the preformattedness
        // of <pre> tags when their innerHTML is assigned.
        // http://stud3.tuwien.ac.at/~e0226430/innerHtmlQuirk.html
        // and it serves to undo the conversion of <br>s to newlines done in
        // chunkify.
        var htmlChunk = textToHtml(
            tabExpander(sourceText.substring(outputIdx, sourceIdx)))
            .replace(lastWasSpace
                     ? startOrSpaceRe
                     : adjacentSpaceRe, '$1&nbsp;');
        // Keep track of whether we need to escape space at the beginning of the
        // next chunk.
        lastWasSpace = trailingSpaceRe.test(htmlChunk);
        // IE collapses multiple adjacient <br>s into 1 line break.
        // Prefix every <br> with '&nbsp;' can prevent such IE's behavior.
        var lineBreakHtml = window['_pr_isIE6']() ? '&nbsp;<br />' : '<br />';
        html.push(htmlChunk.replace(newlineRe, lineBreakHtml));
        outputIdx = sourceIdx;
      }
    }

    while (true) {
      // Determine if we're going to consume a tag this time around.  Otherwise
      // we consume a decoration or exit.
      var outputTag;
      if (tagPos < extractedTags.length) {
        if (decPos < decorations.length) {
          // Pick one giving preference to extractedTags since we shouldn't open
          // a new style that we're going to have to immediately close in order
          // to output a tag.
          outputTag = extractedTags[tagPos] <= decorations[decPos];
        } else {
          outputTag = true;
        }
      } else {
        outputTag = false;
      }
      // Consume either a decoration or a tag or exit.
      if (outputTag) {
        emitTextUpTo(extractedTags[tagPos]);
        if (openDecoration) {
          // Close the current decoration
          html.push('</span>');
          openDecoration = null;
        }
        html.push(extractedTags[tagPos + 1]);
        tagPos += 2;
      } else if (decPos < decorations.length) {
        emitTextUpTo(decorations[decPos]);
        currentDecoration = decorations[decPos + 1];
        decPos += 2;
      } else {
        break;
      }
    }
    emitTextUpTo(sourceText.length);
    if (openDecoration) {
      html.push('</span>');
    }
    job.prettyPrintedHtml = html.join('');
  }

  /** Maps language-specific file extensions to handlers. */
  var langHandlerRegistry = {};
  /** Register a language handler for the given file extensions.
    * @param {function (Object)} handler a function from source code to a list
    *      of decorations.  Takes a single argument job which describes the
    *      state of the computation.   The single parameter has the form
    *      {@code {
    *        source: {string} as plain text.
    *        decorations: {Array.<number|string>} an array of style classes
    *                     preceded by the position at which they start in
    *                     job.source in order.
    *                     The language handler should assigned this field.
    *        basePos: {int} the position of source in the larger source chunk.
    *                 All positions in the output decorations array are relative
    *                 to the larger source chunk.
    *      } }
    * @param {Array.<string>} fileExtensions
    */
  function registerLangHandler(handler, fileExtensions) {
    for (var i = fileExtensions.length; --i >= 0;) {
      var ext = fileExtensions[i];
      if (!langHandlerRegistry.hasOwnProperty(ext)) {
        langHandlerRegistry[ext] = handler;
      } else if ('console' in window) {
        console.warn('cannot override language handler %s', ext);
      }
    }
  }
  function langHandlerForExtension(extension, source) {
    if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
      // Treat it as markup if the first non whitespace character is a < and
      // the last non-whitespace character is a >.
      extension = /^\s*</.test(source)
          ? 'default-markup'
          : 'default-code';
    }
    return langHandlerRegistry[extension];
  }
  registerLangHandler(decorateSource, ['default-code']);
  registerLangHandler(
      createSimpleLexer(
          [],
          [
           [PR_PLAIN,       /^[^<?]+/],
           [PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
           [PR_COMMENT,     /^<\!--[\s\S]*?(?:-\->|$)/],
           // Unescaped content in an unknown language
           ['lang-',        /^<\?([\s\S]+?)(?:\?>|$)/],
           ['lang-',        /^<%([\s\S]+?)(?:%>|$)/],
           [PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
           ['lang-',        /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
           // Unescaped content in javascript.  (Or possibly vbscript).
           ['lang-js',      /^<script\b[^>]*>([\s\S]+?)<\/script\b[^>]*>/i],
           // Contains unescaped stylesheet content
           ['lang-css',     /^<style\b[^>]*>([\s\S]+?)<\/style\b[^>]*>/i],
           ['lang-in.tag',  /^(<\/?[a-z][^<>]*>)/i]
          ]),
      ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
  registerLangHandler(
      createSimpleLexer(
          [
           [PR_PLAIN,        /^[\s]+/, null, ' \t\r\n'],
           [PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
           ],
          [
           [PR_TAG,          /^^<\/?[a-z](?:[\w:-]*\w)?|\/?>$/],
           [PR_ATTRIB_NAME,  /^(?!style\b|on)[a-z](?:[\w:-]*\w)?/],
           ['lang-uq.val',   /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
           [PR_PUNCTUATION,  /^[=<>\/]+/],
           ['lang-js',       /^on\w+\s*=\s*\"([^\"]+)\"/i],
           ['lang-js',       /^on\w+\s*=\s*\'([^\']+)\'/i],
           ['lang-js',       /^on\w+\s*=\s*([^\"\'>\s]+)/i],
           ['lang-css',      /^sty\w+\s*=\s*\"([^\"]+)\"/i],
           ['lang-css',      /^sty\w+\s*=\s*\'([^\']+)\'/i],
           ['lang-css',      /^sty\w+\s*=\s*([^\"\'>\s]+)/i]
           ]),
      ['in.tag']);
  registerLangHandler(
      createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
  registerLangHandler(sourceDecorator({
          'keywords': CPP_KEYWORDS,
          'hashComments': true,
          'cStyleComments': true
        }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
  registerLangHandler(sourceDecorator({
          'keywords': 'null true false'
        }), ['json']);
  registerLangHandler(sourceDecorator({
          'keywords': CSHARP_KEYWORDS,
          'hashComments': true,
          'cStyleComments': true
        }), ['cs']);
  registerLangHandler(sourceDecorator({
          'keywords': JAVA_KEYWORDS,
          'cStyleComments': true
        }), ['java']);
  registerLangHandler(sourceDecorator({
          'keywords': SH_KEYWORDS,
          'hashComments': true,
          'multiLineStrings': true
        }), ['bsh', 'csh', 'sh']);
  registerLangHandler(sourceDecorator({
          'keywords': PYTHON_KEYWORDS,
          'hashComments': true,
          'multiLineStrings': true,
          'tripleQuotedStrings': true
        }), ['cv', 'py']);
  registerLangHandler(sourceDecorator({
          'keywords': PERL_KEYWORDS,
          'hashComments': true,
          'multiLineStrings': true,
          'regexLiterals': true
        }), ['perl', 'pl', 'pm']);
  registerLangHandler(sourceDecorator({
          'keywords': RUBY_KEYWORDS,
          'hashComments': true,
          'multiLineStrings': true,
          'regexLiterals': true
        }), ['rb']);
  registerLangHandler(sourceDecorator({
          'keywords': JSCRIPT_KEYWORDS,
          'cStyleComments': true,
          'regexLiterals': true
        }), ['js']);
  registerLangHandler(
      createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);

  function applyDecorator(job) {
    var sourceCodeHtml = job.sourceCodeHtml;
    var opt_langExtension = job.langExtension;

    // Prepopulate output in case processing fails with an exception.
    job.prettyPrintedHtml = sourceCodeHtml;

    try {
      // Extract tags, and convert the source code to plain text.
      var sourceAndExtractedTags = extractTags(sourceCodeHtml);
      /** Plain text. @type {string} */
      var source = sourceAndExtractedTags.source;
      job.source = source;
      job.basePos = 0;

      /** Even entries are positions in source in ascending order.  Odd entries
        * are tags that were extracted at that position.
        * @type {Array.<number|string>}
        */
      job.extractedTags = sourceAndExtractedTags.tags;

      // Apply the appropriate language handler
      langHandlerForExtension(opt_langExtension, source)(job);
      // Integrate the decorations and tags back into the source code to produce
      // a decorated html string which is left in job.prettyPrintedHtml.
      recombineTagsAndDecorations(job);
    } catch (e) {
      if ('console' in window) {
        console.log(e);
        console.trace();
      }
    }
  }

  function prettyPrintOne(sourceCodeHtml, opt_langExtension) {
    var job = {
      sourceCodeHtml: sourceCodeHtml,
      langExtension: opt_langExtension
    };
    applyDecorator(job);
    return job.prettyPrintedHtml;
  }

  function prettyPrint(opt_whenDone) {
    var isIE6 = window['_pr_isIE6']();

    // fetch a list of nodes to rewrite
    var codeSegments = [
        document.getElementsByTagName('pre'),
        document.getElementsByTagName('code'),
        document.getElementsByTagName('xmp') ];
    var elements = [];
    for (var i = 0; i < codeSegments.length; ++i) {
      for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
        elements.push(codeSegments[i][j]);
      }
    }
    codeSegments = null;

    var clock = Date;
    if (!clock['now']) {
      clock = { 'now': function () { return (new Date).getTime(); } };
    }

    // The loop is broken into a series of continuations to make sure that we
    // don't make the browser unresponsive when rewriting a large page.
    var k = 0;
    var prettyPrintingJob;

    function doWork() {
      var endTime = (window['PR_SHOULD_USE_CONTINUATION'] ?
                     clock.now() + 250 /* ms */ :
                     Infinity);
      for (; k < elements.length && clock.now() < endTime; k++) {
        var cs = elements[k];
        if (cs.className && cs.className.indexOf('prettyprint') >= 0) {
          // If the classes includes a language extensions, use it.
          // Language extensions can be specified like
          //     <pre class="prettyprint lang-cpp">
          // the language extension "cpp" is used to find a language handler as
          // passed to PR_registerLangHandler.
          var langExtension = cs.className.match(/\blang-(\w+)\b/);
          if (langExtension) { langExtension = langExtension[1]; }

          // make sure this is not nested in an already prettified element
          var nested = false;
          for (var p = cs.parentNode; p; p = p.parentNode) {
            if ((p.tagName === 'pre' || p.tagName === 'code' ||
                 p.tagName === 'xmp') &&
                p.className && p.className.indexOf('prettyprint') >= 0) {
              nested = true;
              break;
            }
          }
          if (!nested) {
            // fetch the content as a snippet of properly escaped HTML.
            // Firefox adds newlines at the end.
            var content = getInnerHtml(cs);
            content = content.replace(/(?:\r\n?|\n)$/, '');

            // do the pretty printing
            prettyPrintingJob = {
              sourceCodeHtml: content,
              langExtension: langExtension,
              sourceNode: cs
            };
            applyDecorator(prettyPrintingJob);
            replaceWithPrettyPrintedHtml();
          }
        }
      }
      if (k < elements.length) {
        // finish up in a continuation
        setTimeout(doWork, 250);
      } else if (opt_whenDone) {
        opt_whenDone();
      }
    }

    function replaceWithPrettyPrintedHtml() {
      var newContent = prettyPrintingJob.prettyPrintedHtml;
      if (!newContent) { return; }
      var cs = prettyPrintingJob.sourceNode;

      // push the prettified html back into the tag.
      if (!isRawContent(cs)) {
        // just replace the old html with the new
        cs.innerHTML = newContent;
      } else {
        // we need to change the tag to a <pre> since <xmp>s do not allow
        // embedded tags such as the span tags used to attach styles to
        // sections of source code.
        var pre = document.createElement('PRE');
        for (var i = 0; i < cs.attributes.length; ++i) {
          var a = cs.attributes[i];
          if (a.specified) {
            var aname = a.name.toLowerCase();
            if (aname === 'class') {
              pre.className = a.value;  // For IE 6
            } else {
              pre.setAttribute(a.name, a.value);
            }
          }
        }
        pre.innerHTML = newContent;

        // remove the old
        cs.parentNode.replaceChild(pre, cs);
        cs = pre;
      }

      // Replace <br>s with line-feeds so that copying and pasting works
      // on IE 6.
      // Doing this on other browsers breaks lots of stuff since \r\n is
      // treated as two newlines on Firefox, and doing this also slows
      // down rendering.
      if (isIE6 && cs.tagName === 'PRE') {
        var lineBreaks = cs.getElementsByTagName('br');
        for (var j = lineBreaks.length; --j >= 0;) {
          var lineBreak = lineBreaks[j];
          lineBreak.parentNode.replaceChild(
              document.createTextNode('\r'), lineBreak);
        }
      }
    }

    doWork();
  }

  window['PR_normalizedHtml'] = normalizedHtml;
  window['prettyPrintOne'] = prettyPrintOne;
  window['prettyPrint'] = prettyPrint;
  window['PR'] = {
        'combinePrefixPatterns': combinePrefixPatterns,
        'createSimpleLexer': createSimpleLexer,
        'registerLangHandler': registerLangHandler,
        'sourceDecorator': sourceDecorator,
        'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
        'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
        'PR_COMMENT': PR_COMMENT,
        'PR_DECLARATION': PR_DECLARATION,
        'PR_KEYWORD': PR_KEYWORD,
        'PR_LITERAL': PR_LITERAL,
        'PR_NOCODE': PR_NOCODE,
        'PR_PLAIN': PR_PLAIN,
        'PR_PUNCTUATION': PR_PUNCTUATION,
        'PR_SOURCE': PR_SOURCE,
        'PR_STRING': PR_STRING,
        'PR_TAG': PR_TAG,
        'PR_TYPE': PR_TYPE
      };
})();
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/releases/����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022432� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/releases/index.html������������������������������������0000644�0000765�0000024�00000012701�11313464306�024421� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />	
        <title>LamsonProject: Lamson Releases</title>
        <meta name="keywords" content="python, email, smtp, automation, framework, auto responder, autoresponder, email agent, smtp agent, email engine, email processor, email processing, email proxy, smtp proxy">
        <meta name="description" content="Lamson is a email automation framework written in Python. It can be used to automate email processing and simplify email application development."> 
        <link rel="stylesheet" href="/styles/global.css" type="text/css" charset="utf-8" />
        <link rel="stylesheet" href="/css/code.css" type="text/css" charset="utf-8" />
		<!--[if IE 7]>
		<style type="text/css" media="screen">
			div#column_left ul.sidebar_menu li div.color{
				display: none;
			}
		</style>
        <![endif]-->

        <link href="/prettify.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="/prettify.js"></script>
		
	</head>
	<body onload="prettyPrint()">
		<div id="content_centered">			
			<div id="header">
				<h1><img id="logo" src="/images/lamson.png" alt="Lamson Project(TM) - Pipes and aliases are so 1970." /></h1>
				<ul id="header_menu">
					<li><a href="/">Home</a></li>
					<li><a href="/blog/">News</a></li>
                    <li><a href="/feed.xml">Feed</a></li>
					<li><a href="/download.html">Download</a></li>
					<li><a href="/docs/">Documentation</a></li>
					<li><a href="/docs/api/">API</a></li>
				</ul>
			</div>


            <div id="main_content">
                <h1>Lamson Releases</h1>
                	<p>You can get the Lamson source in various ways:</p>

	<ul>
		<li>Download it from <a href="http://support.lamsonproject.org/vinfo/1.0pre11">support site 1.0pre11 tag</a> as a <a href="http://support.lamsonproject.org/zip/Lamson%20Project-1.0pre11.zip?uuid=1.0pre11">Zip file</a></li>
		<li>Use <a href="http://fossil-scm.org">Fossil</a> to get the code as described on the <a href="http://support.lamsonproject.org/home">support site</a></li>
	</ul>

	<p>And don&#8217;t forget you can install it with easy_install as well:</p>

<pre class="code">
sudo easy_install --upgrade lamson
</pre>

	<blockquote>
		<p>If easy_install doesn&#8217;t work, try without &#8212;upgrade, and if that doesn&#8217;t work then
look in your site-packages directory and delete all the versions of Lamson you find.</p>
	</blockquote>

	<h2>Releases</h2>

	<p>You can get the 1.0pre11 release source from the this site as well:</p>

	<p><a href="/releases/lamson-1.0pre11.tar.gz">1.0pre11 release tar.gz</a></p>

	<p><a href="/releases/lamson-1.0pre11-py2.5.egg">1.0pre11 release Python Egg</a></p>

	<h2>Examples</h2>

	<p>You can get all of the examples from the source distribution.</p>


			</div>

			<div id="column_left">
				<ul class="sidebar_menu">
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff0000;">&nbsp;</div>
                            <a href="/blog/">Latest News</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff9900;">&nbsp;</div>
							<a href="/download.html">Download the Gear</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #99cc00;">&nbsp;</div>
							<a href="/docs/getting_started.html">Getting Started</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #3399ff;">&nbsp;</div>
							<a href="/docs/">Documentation</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff3399;">&nbsp;</div>
							<a href="/docs/faq.html">Frequently Asked Questions</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #006699;">&nbsp;</div>
							<a href="/about.html">About Lamson</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #0099cc;">&nbsp;</div>
							<a href="/contact.html">Getting Help with Lamson</a>
						</div>
					</li>
				</ul>
				
				<div class="sidebar_item">
					<h3>Quick Start</h3>
					<p>See the download instructions for information on getting lamson, and read the getting started instructions to start your own application in less than 10 minutes.</p>
                </div>

                <br/>

				<div class="sidebar_item">
					<h3>Mailing Lists</h3>
                    <p>Lamson hosts its own <a href="/lists/">mailing lists</a> as well as provides a free open mailing list 
                    service for anyone who needs one.  Simply send an email to the list you want @librelist.com and it will
                    get you started.</p>
				</div>
				
			</div>
			
			<div id="footer">
				<div class="footer_content">
                    Lamson Project(TM) and all material on this site Copyright &copy; 2009 <a href="http://zedshaw.com/" title="Zed Shaw's blog">Zed Shaw</a> unless otherwise stated.<br/>
                    
                    Website Designed by <a href="http://kenkeiter.com/">Kenneth Keitner</a> and donated to the LamsonProject.
				</div>
			</div>
			
			<!-- end:centered_content -->
		</div>
	</body>
</html>
	
���������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/releases/index.txt�������������������������������������0000644�0000765�0000024�00000001723�11313464030�024270� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Title: Lamson Releases

You can get the Lamson source in various ways:

* Download it from "support site 1.0pre11 tag":http://support.lamsonproject.org/vinfo/1.0pre11 as a "Zip file":http://support.lamsonproject.org/zip/Lamson%20Project-1.0pre11.zip?uuid=1.0pre11
* Use "Fossil":http://fossil-scm.org to get the code as described on the "support site":http://support.lamsonproject.org/home


And don't forget you can install it with easy_install as well:

<pre class="code">
sudo easy_install --upgrade lamson
</pre>

bq. If easy_install doesn't work, try without --upgrade, and if that doesn't work then
look in your site-packages directory and delete all the versions of Lamson you find.


h2. Releases

You can get the 1.0pre11 release source from the this site as well:

"1.0pre11 release tar.gz":/releases/lamson-1.0pre11.tar.gz

"1.0pre11 release Python Egg":/releases/lamson-1.0pre11-py2.5.egg

h2. Examples

You can get all of the examples from the source distribution.

���������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/styles/������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022152� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/styles/global.css��������������������������������������0000644�0000765�0000024�00000011622�11205001621�024102� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ZED @override http://lamsonproject.com/styles/global.css */

@import url('reset.css');

body{
	background-color: #efefef;
	font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
	font-size: 12px;
	height: 100%;
}

h1{
	font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
	font-size: 22px;
    margin-bottom: 10px;
}

h2, h3, h4, h5, h6{
	padding: 0px !important;
	font-family: Georgia, "Times New Roman", Times, serif;
    padding-top: 1em;
    margin-bottom: 10px;
}

h2{
	padding: 0px;
	font-family: Georgia, "Times New Roman", Times, serif;
	font-size: 18px; font-weight: normal; margin-bottom: 10px;
}

pre.padded{
	margin-bottom: 16px;
}

p{
	line-height: 20px;
	font-size: 12px;
}

p.lightened{
	color: #666;
}

p.padded{
	margin-bottom: 16px;
}



div#content_centered{
	width: 890px;
	margin: 0 auto;
}

/* @group Header */

div#header{
	width: 100%;
	clear: both;
	height: 110px;
	position: relative;
}

div#header img#logo{
	position: absolute;
	top: 22px;
}

div#header ul#header_menu{
	list-style: none;
	display: inline;
	padding: 0;
	margin: 0;
	position: absolute;
	top: 44px;
	right: 34px;
}

div#header ul#header_menu li{
	float: left;
	padding: 3px 20px 3px 0px;
	display: inline;
}
	
	ul#header_menu li a{
		text-decoration: underline;
		padding: 6px 8px 6px 8px;
		color: #333;
	}
	
    ul#header_menu li a:hover{
        color:#fff;
        text-decoration: none;
        background-color: #0099ff;
    }
    
    ul#header_menu li a.selected{
        color:#fff;
        text-decoration: none;
        background-color: #0099ff;
    }

/* @end */

/* @group Masthead */

div#masthead{
	width: 100%;
	background-color: #fff;
	margin-bottom: 36px;
	clear: both;
}

div#masthead h2{
	font-size: 19px;
	color: #333;
}

div#masthead p{
	font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
	line-height: 18px;
}

div#masthead div.content{
	padding: 16px;
}

div#masthead div.features{
	float: right;
	width: 338px;
	margin-bottom: 30px;
	margin-left: 30px;
	margin-right: 44px;
	background-color: #ccc;
	min-height: 178px;
	padding: 16px;
	position: relative;
	top: -10px;
}

div#masthead div.features ul{
	list-style-type: square;
	margin-left: 16px;
}

div#masthead div.features ul li{
	margin-bottom: 10px;
	position: relative;
}

div#masthead a#getstarted{
	position: absolute;
	bottom: 0px;
	right: 10px;
	background-color: #666;
	color: #fff;
	padding: 4px 8px 4px 8px;
	font-family: Georgia, "Times New Roman", Times, serif;
	text-decoration: none;
}

div#masthead a#getstarted:hover{
	background-color: #0099ff;
}

/* @end */

/* @group Columns */

div#column_left{
	margin-right: 36px;
	float: left;
	width: 240px;
	height: 100%;
}

div#main_content{
	width: 614px;
	float: right;
}

/* @end */

/* @group Sidebar */

/* @group Sidebar Item */

div#column_left div.sidebar_item{
	padding: 6px;
	background-color: #fff;
}

div#column_left div.sidebar_item h3{
	font-size: 16px;
	margin: 0px 0px 4px 0px;
}

div#column_left div.sidebar_item p{
	font-size: 11px;
	line-height: 14px;
}

/* @end */

div#column_left ul.sidebar_menu{
	margin-bottom: 36px;
}

div#column_left ul.sidebar_menu li{
	display: block;
}

div#column_left ul.sidebar_menu li div.color{
	width: 3px;
	float: left;
	height: 100%;
	position: relative;
	display: inline;
}

div#column_left ul.sidebar_menu li div.item a{
	text-decoration: none;
	background-color: #ccc;
	font-size: 12px;
	color: #333;
	display: block;
	padding: 7px 0px 9px 16px;
	width: 224px;
}

div#column_left ul.sidebar_menu li div.item a:hover{
	background-color: #aaa;
	color: #fff
}

div#column_left ul.sidebar_menu li div.item{
	background-color: #ccc;
    height: 28px;
	width: 100%;
	margin-bottom: 1px;
	zoom: 1 !important;
}

/* @end */

/* @group Content */

div#main_content p{
	margin-bottom: 10px;
}

div#main_content blockquote{
	margin: 4px 0px 10px 0px;
	padding: 4px 0px 4px 10px;
	line-height: 20px;
	border-left: 3px solid #1d7872;
	text-indent: 0px;
	font-family: Georgia, "Times New Roman", Times, serif;
	font-size: 13px;
	font-weight: normal;
}

div#main_content pre.code{
	border-left: 3px solid #666;
	background-color: #333;
	padding: 8px;
	color: #fff;
	font-family: "Courier New", Courier, mono;
	margin-bottom: 10px;
}

div#main_content code{
	color: #000;
	font-weight: bold;
	font-family: "Courier New", Courier, mono;
}

div#main_content ul{
	list-style-type: square;
	padding-left: 14px;
	margin-left: 10px;
	line-height: 18px;
	margin-bottom: 16px;
}

div#main_content ul li{
	padding-bottom: 4px;
}

div#main_content ol{
	padding-left: 14px;
	margin-left: 10px;
	line-height: 18px;
	margin-bottom: 16px;
	list-style-type: decimal-leading-zero;
}

div#main_content ol li{
	padding-bottom: 4px;
}

/* @end */

/* @group Footer */

div#footer{
	text-align: right;
	padding-top: 40px;
	clear: both;
}

div#footer div.footer_content{
	background-color: #fff;
	padding: 16px;
	padding-bottom: 40px;
	color: #999;
	line-height: 20px;
}

div#footer div.footer_content a{
	color: #666;
}



/* @end */
��������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/styles/reset.css���������������������������������������0000644�0000765�0000024�00000001642�11202472526�024002� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
	margin: 0;
	padding: 0;
	border: 0;
	outline: 0;
	font-weight: inherit;
	font-style: inherit;
	font-size: 100%;
	font-family: inherit;
	vertical-align: baseline;
}
/* remember to define focus styles! */
:focus {
	outline: 0;
}
body {
	line-height: 1;
	color: black;
	background: white;
}
ol, ul {
	list-style: none;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
	border-collapse: separate;
	border-spacing: 0;
}
caption, th, td {
	text-align: left;
	font-weight: normal;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: "";
}
blockquote, q {
	quotes: "" "";
}
����������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/videos/������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022120� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/videos/index.html��������������������������������������0000644�0000765�0000024�00000014405�11313464306�024112� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />	
        <title>LamsonProject: Screencasts And Tutorials</title>
        <meta name="keywords" content="python, email, smtp, automation, framework, auto responder, autoresponder, email agent, smtp agent, email engine, email processor, email processing, email proxy, smtp proxy">
        <meta name="description" content="Lamson is a email automation framework written in Python. It can be used to automate email processing and simplify email application development."> 
        <link rel="stylesheet" href="/styles/global.css" type="text/css" charset="utf-8" />
        <link rel="stylesheet" href="/css/code.css" type="text/css" charset="utf-8" />
		<!--[if IE 7]>
		<style type="text/css" media="screen">
			div#column_left ul.sidebar_menu li div.color{
				display: none;
			}
		</style>
        <![endif]-->

        <link href="/prettify.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="/prettify.js"></script>
		
	</head>
	<body onload="prettyPrint()">
		<div id="content_centered">			
			<div id="header">
				<h1><img id="logo" src="/images/lamson.png" alt="Lamson Project(TM) - Pipes and aliases are so 1970." /></h1>
				<ul id="header_menu">
					<li><a href="/">Home</a></li>
					<li><a href="/blog/">News</a></li>
                    <li><a href="/feed.xml">Feed</a></li>
					<li><a href="/download.html">Download</a></li>
					<li><a href="/docs/">Documentation</a></li>
					<li><a href="/docs/api/">API</a></li>
				</ul>
			</div>


            <div id="main_content">
                <h1>Screencasts And Tutorials</h1>
                <p>
This is the first screencast that walks you through the first half of the <a href="/docs/deploying_lamson.html">Deploying Lamson</a>
tutorial.  It is the first screencast so it's probably rough and the sound isn't so great.  If you have tips and feedback on doing these
better <a href="/contact.html">contact me</a> and let me know.
</p>

<embed type="application/x-shockwave-flash" width="640" 	height="480" 	allowfullscreen="true" 	allowscriptaccess="always" 	src="http://www.archive.org/flow/flowplayer.commercial-3.0.5.swf" 	w3c="true" 	flashvars='config={"key":"#$b6eb72a0f2f1e29f3d4","playlist":[{"url":"http://www.archive.org/download/Lamsonproject.orgDeployLamsonInAVirtualenv/format=Thumbnail?.jpg","autoPlay":true,"scaling":"fit"},{"url":"http://www.archive.org/download/Lamsonproject.orgDeployLamsonInAVirtualenv/deploy_lamson_virtual_env.m4v","autoPlay":false,"accelerated":true,"scaling":"fit","provider":"h264streaming"}],"clip":{"autoPlay":false,"accelerated":true,"scaling":"fit","provider":"h264streaming"},"canvas":{"backgroundColor":"0x000000","backgroundGradient":"none"},"plugins":{"audio":{"url":"http://www.archive.org/flow/flowplayer.audio-3.0.3-dev.swf"},"controls":{"playlist":false,"fullscreen":true,"gloss":"high","backgroundColor":"0x000000","backgroundGradient":"medium","sliderColor":"0x777777","progressColor":"0x777777","timeColor":"0xeeeeee","durationColor":"0x01DAFF","buttonColor":"0x333333","buttonOverColor":"0x505050"},"h264streaming":{"url":"http://www.archive.org/flow/flowplayer.h264streaming-3.0.5.swf"}},"contextMenu":[{"Item Lamsonproject.orgDeployLamsonInAVirtualenv at archive.org":"function()"},"-","Flowplayer 3.0.5"]}'> </embed>

<p><B>WARNING: Seeking seems to not work with this player using the highquality video.  Just let it stream or download it.</b></p>

<p>
You can <a href="http://www.archive.org/details/Lamsonproject.orgDeployLamsonInAVirtualenv">download this video from archive.org</a> to get a better quality video.
</p>

			</div>

			<div id="column_left">
				<ul class="sidebar_menu">
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff0000;">&nbsp;</div>
                            <a href="/blog/">Latest News</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff9900;">&nbsp;</div>
							<a href="/download.html">Download the Gear</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #99cc00;">&nbsp;</div>
							<a href="/docs/getting_started.html">Getting Started</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #3399ff;">&nbsp;</div>
							<a href="/docs/">Documentation</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff3399;">&nbsp;</div>
							<a href="/docs/faq.html">Frequently Asked Questions</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #006699;">&nbsp;</div>
							<a href="/about.html">About Lamson</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #0099cc;">&nbsp;</div>
							<a href="/contact.html">Getting Help with Lamson</a>
						</div>
					</li>
				</ul>
				
				<div class="sidebar_item">
					<h3>Quick Start</h3>
					<p>See the download instructions for information on getting lamson, and read the getting started instructions to start your own application in less than 10 minutes.</p>
                </div>

                <br/>

				<div class="sidebar_item">
					<h3>Mailing Lists</h3>
                    <p>Lamson hosts its own <a href="/lists/">mailing lists</a> as well as provides a free open mailing list 
                    service for anyone who needs one.  Simply send an email to the list you want @librelist.com and it will
                    get you started.</p>
				</div>
				
			</div>
			
			<div id="footer">
				<div class="footer_content">
                    Lamson Project(TM) and all material on this site Copyright &copy; 2009 <a href="http://zedshaw.com/" title="Zed Shaw's blog">Zed Shaw</a> unless otherwise stated.<br/>
                    
                    Website Designed by <a href="http://kenkeiter.com/">Kenneth Keitner</a> and donated to the LamsonProject.
				</div>
			</div>
			
			<!-- end:centered_content -->
		</div>
	</body>
</html>
	
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/output/videos/index.txt���������������������������������������0000644�0000765�0000024�00000004000�11213304060�023740� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Title:  Screencasts And Tutorials
Content-Type: text/html

<p>
This is the first screencast that walks you through the first half of the <a href="/docs/deploying_lamson.html">Deploying Lamson</a>
tutorial.  It is the first screencast so it's probably rough and the sound isn't so great.  If you have tips and feedback on doing these
better <a href="/contact.html">contact me</a> and let me know.
</p>

<embed type="application/x-shockwave-flash" width="640" 	height="480" 	allowfullscreen="true" 	allowscriptaccess="always" 	src="http://www.archive.org/flow/flowplayer.commercial-3.0.5.swf" 	w3c="true" 	flashvars='config={"key":"#$b6eb72a0f2f1e29f3d4","playlist":[{"url":"http://www.archive.org/download/Lamsonproject.orgDeployLamsonInAVirtualenv/format=Thumbnail?.jpg","autoPlay":true,"scaling":"fit"},{"url":"http://www.archive.org/download/Lamsonproject.orgDeployLamsonInAVirtualenv/deploy_lamson_virtual_env.m4v","autoPlay":false,"accelerated":true,"scaling":"fit","provider":"h264streaming"}],"clip":{"autoPlay":false,"accelerated":true,"scaling":"fit","provider":"h264streaming"},"canvas":{"backgroundColor":"0x000000","backgroundGradient":"none"},"plugins":{"audio":{"url":"http://www.archive.org/flow/flowplayer.audio-3.0.3-dev.swf"},"controls":{"playlist":false,"fullscreen":true,"gloss":"high","backgroundColor":"0x000000","backgroundGradient":"medium","sliderColor":"0x777777","progressColor":"0x777777","timeColor":"0xeeeeee","durationColor":"0x01DAFF","buttonColor":"0x333333","buttonOverColor":"0x505050"},"h264streaming":{"url":"http://www.archive.org/flow/flowplayer.h264streaming-3.0.5.swf"}},"contextMenu":[{"Item Lamsonproject.orgDeployLamsonInAVirtualenv at archive.org":"function()"},"-","Flowplayer 3.0.5"]}'> </embed>

<p><B>WARNING: Seeking seems to not work with this player using the highquality video.  Just let it stream or download it.</b></p>

<p>
You can <a href="http://www.archive.org/details/Lamsonproject.orgDeployLamsonInAVirtualenv">download this video from archive.org</a> to get a better quality video.
</p>
lamson-1.0pre11/doc/lamsonproject.org/Session.vim���������������������������������������������������0000644�0000765�0000024�00000021776�11203577062�021437� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������let SessionLoad = 1
if &cp | set nocp | endif
let s:cpo_save=&cpo
set cpo&vim
inoremap <F11> :syntax sync fromstart
a
cmap <D-g> <D-g>
imap <D-g> <D-g>
cmap <D-f> <D-f>
imap <D-f> <D-f>
cmap <D-a> <D-a>
imap <D-a> <D-a>
cnoremap <D-v> +
cnoremap <D-c> 
cmap <D-z> <D-z>
imap <D-z> <D-z>
cmap <S-D-s> <D-s>
imap <S-D-s> <D-s>
cmap <D-s> <D-s>
imap <D-s> <D-s>
cmap <D-w> <D-w>
imap <D-w> <D-w>
cmap <D-o> <D-o>
imap <D-o> <D-o>
cmap <D-n> <D-n>
imap <D-n> <D-n>
map  :cstag =expand("<cword>")

xmap S <Plug>VSurround
map \dk <Plug>DirDiffPrev
map \dj <Plug>DirDiffNext
map \dp <Plug>DirDiffPut
map \dg <Plug>DirDiffGet
nmap \S :setlocal spell spelllang=en_us
nmap \\i :cs find i =expand("<cword>")

nmap \\f :cs find f =expand("<cword>")

nmap \\e :cs find e 
nmap \\t :cs find t 
nmap \\c :cs find c =expand("<cword>")

nmap \\d :cs find d =expand("<cword>")

nmap \\g :cs find g =expand("<cword>")

nmap \\s :cs find s =expand("<cword>")

nmap <silent> \; d/;/e
:noh
nmap <silent> \n :cn
nmap <silent> \q :copen
nmap \d :w
:diffoff
nmap \D :w
:diffthis
nmap \M :wa
:make!
nmap \U :TlistUpdate
nmap <silent> \B /end
=%:noh
nmap cs <Plug>Csurround
nmap ds <Plug>Dsurround
nmap gx <Plug>NetrwBrowseX
xmap s <Plug>Vsurround
nmap ySS <Plug>YSsurround
nmap ySs <Plug>YSsurround
nmap yss <Plug>Yssurround
nmap yS <Plug>YSurround
nmap ys <Plug>Ysurround
nnoremap <silent> <Plug>NetrwBrowseX :call netrw#NetBrowseX(expand("<cWORD>"),0)
nmap <F12> :wa
:mksession!
:qa
noremap <F11> :syntax sync fromstart
nmap <silent> <M-S-Left> :bp
omap <D-g> <D-g>
vmap <D-g> <D-g>
nnoremap <D-g> n
omap <D-f> <D-f>
vmap <D-f> <D-f>
nnoremap <D-f> /
omap <D-a> <D-a>
vmap <D-a> <D-a>
nnoremap <silent> <D-a> :if &slm != ""|exe ":norm gggHG"| else|exe ":norm ggVG"|endif
omap <D-z> <D-z>
vmap <D-z> <D-z>gv
nnoremap <D-z> u
omap <S-D-s> <D-s>
vmap <S-D-s> <D-s>gv
nnoremap <S-D-s> :browse confirm saveas
omap <D-s> <D-s>
vmap <D-s> <D-s>gv
nnoremap <silent> <D-s> :if expand("%") == ""|browse confirm w| else|confirm w|endif
omap <D-w> <D-w>
vmap <D-w> <D-w>gv
nnoremap <silent> <D-w> :if winheight(2) < 0 | confirm enew | else | confirm close | endif
omap <D-o> <D-o>
vmap <D-o> <D-o>gv
nnoremap <D-o> :browse confirm e
omap <D-n> <D-n>
vmap <D-n> <D-n>gv
nnoremap <D-n> :confirm enew
vmap <BS> "-d
vnoremap <D-x> "+x
vnoremap <D-c> "+y
nnoremap <D-v> "+gP
imap S <Plug>ISurround
imap s <Plug>Isurround
imap  <Plug>Isurround
let &cpo=s:cpo_save
unlet s:cpo_save
set autoindent
set backspace=2
set cindent
set cscopetag
set cscopeverbose
set expandtab
set formatoptions=cq
set grepprg=ack
set helplang=en
set hidden
set incsearch
set laststatus=2
set makeprg=vellum
set mouse=a
set mousemodel=popup
set shell=bash
set shiftwidth=4
set smartindent
set smarttab
set softtabstop=4
set statusline=%<%f%=\ [%1*%M%*%n%R]\ y\ %-19(%3l,%02c%03V%)
set tabstop=4
set tags=tags;/home/zedshaw
set whichwrap=b,s,<,>,h,l
set wildignore=*.pyc
set window=56
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
let v:this_session=expand("<sfile>:p")
silent only
cd ~/projects/lamson/doc/lamsonproject.org
if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == ''
  let s:wipebuf = bufnr('%')
endif
set shortmess=aoO
badd +1 input/index.txt
badd +14 input/blog/2009-05-16.txt
badd +4 input/download.txt
badd +4 input/docs/getting_started.txt
badd +6 input/blog/index.txt
badd +1 input/blog/template.html
badd +1 input/docs/template.html
badd +1 input/lists/index.txt
badd +4 input/about.txt
badd +120 template.html
badd +4 input/contact.txt
badd +223 output/css/style.css
badd +0 http://high5.net/mirrors/minimalist/README
silent! argdel *
edit http://high5.net/mirrors/minimalist/README
set splitbelow splitright
wincmd _ | wincmd |
vsplit
1wincmd h
wincmd w
set nosplitbelow
set nosplitright
wincmd t
set winheight=1 winwidth=1
exe 'vert 1resize ' . ((&columns * 103 + 103) / 206)
exe 'vert 2resize ' . ((&columns * 102 + 103) / 206)
argglobal
setlocal noarabic
setlocal autoindent
setlocal autoread
setlocal nobinary
setlocal bufhidden=
setlocal buflisted
setlocal buftype=
setlocal cindent
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
setlocal cinoptions=
setlocal cinwords=if,else,while,do,for,switch
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
setlocal commentstring=/*%s*/
setlocal complete=.,w,b,u,t,i
setlocal completefunc=
setlocal nocopyindent
setlocal nocursorcolumn
setlocal nocursorline
setlocal define=
setlocal dictionary=
setlocal nodiff
setlocal equalprg=
setlocal errorformat=
setlocal expandtab
if &filetype != ''
setlocal filetype=
endif
setlocal foldcolumn=0
setlocal foldenable
setlocal foldexpr=0
setlocal foldignore=#
setlocal foldlevel=0
setlocal foldmarker={{{,}}}
setlocal foldmethod=manual
setlocal foldminlines=1
setlocal foldnestmax=20
setlocal foldtext=foldtext()
setlocal formatexpr=
setlocal formatoptions=cq
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
setlocal grepprg=
setlocal iminsert=2
setlocal imsearch=2
setlocal include=
setlocal includeexpr=
setlocal indentexpr=
setlocal indentkeys=0{,0},:,0#,!^F,o,O,e
setlocal noinfercase
setlocal iskeyword=@,48-57,_,192-255
setlocal keymap=
setlocal keywordprg=
setlocal nolinebreak
setlocal nolisp
setlocal nolist
setlocal makeprg=
setlocal matchpairs=(:),{:},[:]
setlocal modeline
setlocal modifiable
setlocal nrformats=octal,hex
setlocal nonumber
setlocal numberwidth=4
setlocal omnifunc=
setlocal path=
setlocal nopreserveindent
setlocal nopreviewwindow
setlocal quoteescape=\\
setlocal noreadonly
setlocal norightleft
setlocal rightleftcmd=search
setlocal noscrollbind
setlocal shiftwidth=4
setlocal noshortname
setlocal smartindent
setlocal softtabstop=4
setlocal nospell
setlocal spellcapcheck=[.?!]\\_[\\])'\"\	\ ]\\+
setlocal spellfile=
setlocal spelllang=en
setlocal statusline=
setlocal suffixesadd=
setlocal swapfile
setlocal synmaxcol=3000
if &syntax != ''
setlocal syntax=
endif
setlocal tabstop=4
setlocal tags=
setlocal textwidth=0
setlocal thesaurus=
setlocal nowinfixheight
setlocal nowinfixwidth
setlocal wrap
setlocal wrapmargin=0
silent! normal! zE
let s:l = 133 - ((15 * winheight(0) + 27) / 55)
if s:l < 1 | let s:l = 1 | endif
exe s:l
normal! zt
133
normal! 0
wincmd w
argglobal
edit input/blog/template.html
setlocal noarabic
setlocal autoindent
setlocal autoread
setlocal nobinary
setlocal bufhidden=
setlocal buflisted
setlocal buftype=
setlocal cindent
setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e
setlocal cinoptions=
setlocal cinwords=if,else,while,do,for,switch
setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
setlocal commentstring=<!--%s-->
setlocal complete=.,w,b,u,t,i
setlocal completefunc=
setlocal nocopyindent
setlocal nocursorcolumn
setlocal nocursorline
setlocal define=
setlocal dictionary=
setlocal nodiff
setlocal equalprg=
setlocal errorformat=
setlocal expandtab
if &filetype != 'html'
setlocal filetype=html
endif
setlocal foldcolumn=0
setlocal foldenable
setlocal foldexpr=0
setlocal foldignore=#
setlocal foldlevel=0
setlocal foldmarker={{{,}}}
setlocal foldmethod=manual
setlocal foldminlines=1
setlocal foldnestmax=20
setlocal foldtext=foldtext()
setlocal formatexpr=
setlocal formatoptions=tcq
setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s*
setlocal grepprg=
setlocal iminsert=2
setlocal imsearch=2
setlocal include=
setlocal includeexpr=
setlocal indentexpr=HtmlIndentGet(v:lnum)
setlocal indentkeys=o,O,*<Return>,<>>,{,}
setlocal noinfercase
setlocal iskeyword=@,48-57,_,192-255,$
setlocal keymap=
setlocal keywordprg=
setlocal nolinebreak
setlocal nolisp
setlocal nolist
setlocal makeprg=
setlocal matchpairs=(:),{:},[:]
setlocal modeline
setlocal modifiable
setlocal nrformats=octal,hex
setlocal nonumber
setlocal numberwidth=4
setlocal omnifunc=htmlcomplete#CompleteTags
setlocal path=
setlocal nopreserveindent
setlocal nopreviewwindow
setlocal quoteescape=\\
setlocal noreadonly
setlocal norightleft
setlocal rightleftcmd=search
setlocal noscrollbind
setlocal shiftwidth=4
setlocal noshortname
setlocal smartindent
setlocal softtabstop=4
setlocal nospell
setlocal spellcapcheck=[.?!]\\_[\\])'\"\	\ ]\\+
setlocal spellfile=
setlocal spelllang=en
setlocal statusline=
setlocal suffixesadd=
setlocal swapfile
setlocal synmaxcol=3000
if &syntax != 'html'
setlocal syntax=html
endif
setlocal tabstop=4
setlocal tags=
setlocal textwidth=0
setlocal thesaurus=
setlocal nowinfixheight
setlocal nowinfixwidth
setlocal wrap
setlocal wrapmargin=0
silent! normal! zE
let s:l = 1 - ((0 * winheight(0) + 27) / 55)
if s:l < 1 | let s:l = 1 | endif
exe s:l
normal! zt
1
normal! 0
wincmd w
exe 'vert 1resize ' . ((&columns * 103 + 103) / 206)
exe 'vert 2resize ' . ((&columns * 102 + 103) / 206)
tabnext 1
if exists('s:wipebuf')
  silent exe 'bwipe ' . s:wipebuf
endif
unlet! s:wipebuf
set winheight=1 winwidth=20 shortmess=filnxtToO
let s:sx = expand("<sfile>:p:r")."x.vim"
if file_readable(s:sx)
  exe "source " . s:sx
endif
let &so = s:so_save | let &siso = s:siso_save
doautoall SessionLoadPost
unlet SessionLoad
" vim: set ft=vim :
��lamson-1.0pre11/doc/lamsonproject.org/template.html�������������������������������������������������0000644�0000765�0000024�00000010632�11310757665�021775� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />	
        <title>LamsonProject: $title</title>
        <meta name="keywords" content="python, email, smtp, automation, framework, auto responder, autoresponder, email agent, smtp agent, email engine, email processor, email processing, email proxy, smtp proxy">
        <meta name="description" content="Lamson is a email automation framework written in Python. It can be used to automate email processing and simplify email application development."> 
        <link rel="stylesheet" href="/styles/global.css" type="text/css" charset="utf-8" />
        <link rel="stylesheet" href="/css/code.css" type="text/css" charset="utf-8" />
		<!--[if IE 7]>
		<style type="text/css" media="screen">
			div#column_left ul.sidebar_menu li div.color{
				display: none;
			}
		</style>
        <![endif]-->

        <link href="/prettify.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="/prettify.js"></script>
		
	</head>
	<body onload="prettyPrint()">
		<div id="content_centered">			
			<div id="header">
				<h1><img id="logo" src="$baseurl/images/lamson.png" alt="Lamson Project(TM) - Pipes and aliases are so 1970." /></h1>
				<ul id="header_menu">
					<li><a href="$baseurl/">Home</a></li>
					<li><a href="$baseurl/blog/">News</a></li>
                    <li><a href="$baseurl/feed.xml">Feed</a></li>
					<li><a href="$baseurl/download.html">Download</a></li>
					<li><a href="$baseurl/docs/">Documentation</a></li>
					<li><a href="$baseurl/docs/api/">API</a></li>
				</ul>
			</div>


            <div id="main_content">
                <h1>$title</h1>
                $content
			</div>

			<div id="column_left">
				<ul class="sidebar_menu">
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff0000;">&nbsp;</div>
                            <a href="$baseurl/blog/">Latest News</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff9900;">&nbsp;</div>
							<a href="$baseurl/download.html">Download the Gear</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #99cc00;">&nbsp;</div>
							<a href="$baseurl/docs/getting_started.html">Getting Started</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #3399ff;">&nbsp;</div>
							<a href="$baseurl/docs/">Documentation</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #ff3399;">&nbsp;</div>
							<a href="$baseurl/docs/faq.html">Frequently Asked Questions</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #006699;">&nbsp;</div>
							<a href="$baseurl/about.html">About Lamson</a>
						</div>
					</li>
					<li>
						<div class="item">
							<div class="color" style="background-color: #0099cc;">&nbsp;</div>
							<a href="$baseurl/contact.html">Getting Help with Lamson</a>
						</div>
					</li>
				</ul>
				
				<div class="sidebar_item">
					<h3>Quick Start</h3>
					<p>See the download instructions for information on getting lamson, and read the getting started instructions to start your own application in less than 10 minutes.</p>
                </div>

                <br/>

				<div class="sidebar_item">
					<h3>Mailing Lists</h3>
                    <p>Lamson hosts its own <a href="$baseurl/lists/">mailing lists</a> as well as provides a free open mailing list 
                    service for anyone who needs one.  Simply send an email to the list you want @librelist.com and it will
                    get you started.</p>
				</div>
				
			</div>
			
			<div id="footer">
				<div class="footer_content">
                    Lamson Project(TM) and all material on this site Copyright &copy; 2009 <a href="http://zedshaw.com/" title="Zed Shaw's blog">Zed Shaw</a> unless otherwise stated.<br/>
                    
                    Website Designed by <a href="http://kenkeiter.com/">Kenneth Keitner</a> and donated to the LamsonProject.
				</div>
			</div>
			
			<!-- end:centered_content -->
		</div>
	</body>
</html>
	
������������������������������������������������������������������������������������������������������lamson-1.0pre11/doc/lamsonproject.org/webgen.py�����������������������������������������������������0000644�0000765�0000024�00000014132�11224457663�021113� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python
from __future__ import with_statement

import os
import sys
import string
from string import Template
from config import *
from datetime import date
from textile import textile
from stat import *
import datetime
import PyRSS2Gen

rss = PyRSS2Gen.RSS2(
    title = options["sitename"],
    link = options["siteurl"],
    description = options["slogan"],
    lastBuildDate = datetime.datetime.now(),
    items = [])


def add_rss_item(rss, title, link, description, pubDate):
       item = PyRSS2Gen.RSSItem(title = title, link = link,
         description = description,
         guid = PyRSS2Gen.Guid(link),
         pubDate = datetime.datetime.fromtimestamp(pubDate))
       rss.items.append(item)

def ext(fname):
    return os.path.splitext(fname)[1]

def process(fname):
    with open(fname, 'r') as f:
        try:
            head, body = f.read().split('\n\n')
            body
        except:
            print 'Invalid file format : ', fname

def parse(fname):
    with open(fname, 'r') as f:
        raw = f.read()
        headers = {}
        try:
            (header_lines,body) = raw.split("\n\n", 1)
            for header in header_lines.split("\n"):
                (name, value) = header.split(": ", 1)
                headers[name.lower()] = unicode(value.strip())
            return headers, body
        except:
            raise TypeError, "Invalid page file format for %s" % fname

           
def get_template(template):
    """Takes the directory where templates are located and the template name. Returns a blob containing the template."""
    template = os.path.join(template_dir, template)

    return Template(open(template, 'r').read())
       
def source_newer(source, target):
    if len(sys.argv) > 1 and sys.argv[1] == "force":
        return True

    if not os.path.exists(target): 
        return True
    else:
        smtime = os.stat(source)[ST_MTIME]
        tmtime = os.stat(target)[ST_MTIME]
        return smtime > tmtime

def is_blog(current_dir, myself, headers, files):
    """A page tagged as an entry will get the files, sort them by their dates,
    and then the contents will be that directory listing instead."""
    
    if 'content-type' in headers and headers['content-type'] == "text/blog":
        # it's a listing, make it all work
        without_self = files[:]
        without_self.remove(os.path.split(myself)[-1])
        without_self.sort(reverse=True)

        listing = []
        for f in without_self:
            print "Doing blog", f
            # load up the file and peel out the first few paragraphs
            content = os.path.join(current_dir, f)
            head, body = parse(content)
            paras = [p for p in body.split("\n\n") if p]
            if paras:
                # now make a simple listing entry with it
                date, ext = os.path.splitext(f)
                head["link"] = os.path.join("/" + os.path.split(current_dir)[-1], date + ".html")
                head["date"] = date
                format = determine_format(head)
                pubDate = smtime = os.stat(content)[ST_CTIME]
                head["content"] = content_format(current_dir, f, head, files,
                                                 format, "\n\n".join(paras[0:1]))
                template = head['item-template'] if 'item-template' in head else headers['item-template']
                description = get_template(template).safe_substitute(head)

                if "feed" not in headers:
                    add_rss_item(rss, head["title"], options["siteurl"] +
                                 head["link"], description, pubDate)
                listing.append(description)

        return lambda s: "".join(listing)
    else:
        return lambda s: s

def content_format(current_dir, inp, headers, files, format, body):
    return {
            u'text/plain': lambda s: u'<pre>%s</pre>' % s,
            u'text/x-textile':  lambda s: u'%s' % textile(s,head_offset=0, validate=0, 
                                sanitize=0, encoding='utf-8', output='utf-8'),
            u'text/html': lambda s: s,
            u'text/blog': is_blog(current_dir, inp, headers, files)
        }[format](body)

def determine_format(headers):
    if 'content-type' in headers:
        return headers['content-type']
    else:
        return options['format']

def parse_directory(current_dir, files, output_dir):
    files = [f for f in files if ext(f) in options['extensions']]
    for f in files:
        inp = os.path.join(current_dir, f)
        target = os.path.join(output_dir, f)
        # TODO: Allow specifying the target extension from headers
        outp = os.path.splitext(target)[0] + '.html'

        # always redo the indexes since they'll typically list information to
        # update from the directory they are in
        if not source_newer(inp, outp) and f != "index.txt":
            continue

        headers, body = parse(inp)

        if 'template' not in headers:
            blob = get_template(template)
        else:
            blob = get_template(headers['template'])

        format = determine_format(headers)

        print "Processing %s" % inp

        content = content_format(current_dir, inp, headers, files, format, body)
        
        headers['content'] = content
        headers.update(options)
        output = blob.safe_substitute(**headers)

        outf = open(outp, 'w')
        outf.write(output)
        outf.close()

def a_fucking_cmp_for_time(x,y):
    diff = y.pubDate - x.pubDate
    return diff.days * 24 * 60 * 60 + diff.seconds

def main():
    ### Walks through the input dir creating finding all subdirectories.
    for root, dirs, files in os.walk(input_dir):
        output = root.replace(input_dir, output_dir)
        ### Checks if the directory exists in output and creates it if false.
        if not os.path.isdir(output):
            os.makedirs(output)

        parse_directory(root, files, output)

    x,y = rss.items[0], rss.items[-1]
    diff = x.pubDate - y.pubDate
    print "diff!", diff.seconds, diff.days
    rss.items.sort(cmp=lambda x,y: a_fucking_cmp_for_time(x,y))
    rss.write_xml(open("output/feed.xml", "w"))

    
if __name__ == '__main__':
    main()
��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/���������������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464573�014671� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/�����������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016663� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/�������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017443� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/__init__.py��������������������������������������������������0000644�0000765�0000024�00000000000�11225417571�021537� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/handlers/����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021243� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/handlers/__init__.py�����������������������������������������0000644�0000765�0000024�00000000000�11225710421�023325� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/handlers/admin.py��������������������������������������������0000644�0000765�0000024�00000011673�11251127106�022701� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from email.utils import parseaddr
from config.settings import relay, SPAM, CONFIRM
import logging
from lamson import view, queue
from lamson.routing import route, stateless, route_like, state_key_generator
from lamson.bounce import bounce_to
from lamson.server import SMTPError
from lamson.spam import spam_filter
from app.model import mailinglist, bounce, archive
from app.handlers import bounce


INVALID_LISTS = ["noreply", "unbounce"]


@state_key_generator
def module_and_to(module_name, message):
    name, address = parseaddr(message['to'])
    if '-' in address:
        list_name = address.split('-')[0]
    else:
        list_name = address.split('@')[0]

    return module_name + ':' + list_name


@route("(address)@(host)", address='.+')
def SPAMMING(message, **options):
    return SPAMMING


@route('(bad_list)@(host)', bad_list='.+')
@route('(list_name)@(host)')
@route('(list_name)-subscribe@(host)')
@bounce_to(soft=bounce.BOUNCED_SOFT, hard=bounce.BOUNCED_HARD)
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, list_name=None, host=None, bad_list=None):
    if bad_list:
        if '-' in bad_list:
            # probably put a '.' in it, try to find a similar list
            similar_lists = mailinglist.similar_named_lists(bad_list.replace('-','.'))
        else:
            similar_lists = mailinglist.similar_named_lists(bad_list)

        help = view.respond(locals(), "mail/bad_list_name.msg",
                            From="noreply@%(host)s",
                            To=message['from'],
                            Subject="That's not a valid list name.")
        relay.deliver(help)

        return START

    elif list_name in INVALID_LISTS or message['from'].endswith(host):
        logging.debug("LOOP MESSAGE to %r from %r.", message['to'],
                     message['from'])
        return START

    elif mailinglist.find_list(list_name):
        action = "subscribe to"
        CONFIRM.send(relay, list_name, message, 'mail/confirmation.msg',
                          locals())
        return CONFIRMING_SUBSCRIBE

    else:
        similar_lists = mailinglist.similar_named_lists(list_name)
        CONFIRM.send(relay, list_name, message, 'mail/create_confirmation.msg',
                          locals())

        return CONFIRMING_SUBSCRIBE

@route('(list_name)-confirm-(id_number)@(host)')
def CONFIRMING_SUBSCRIBE(message, list_name=None, id_number=None, host=None):
    original = CONFIRM.verify(list_name, message['from'], id_number)

    if original:
        mailinglist.add_subscriber(message['from'], list_name)

        msg = view.respond(locals(), "mail/subscribed.msg",
                           From="noreply@%(host)s",
                           To=message['from'],
                           Subject="Welcome to %(list_name)s list.")
        relay.deliver(msg)

        CONFIRM.cancel(list_name, message['from'], id_number)

        return POSTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING_SUBSCRIBE


@route('(list_name)-(action)@(host)', action='[a-z]+')
@route('(list_name)@(host)')
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def POSTING(message, list_name=None, action=None, host=None):
    if action == 'unsubscribe':
        action = "unsubscribe from"
        CONFIRM.send(relay, list_name, message, 'mail/confirmation.msg',
                          locals())
        return CONFIRMING_UNSUBSCRIBE
    else:
        mailinglist.post_message(relay, message, list_name, host)
        # archive makes sure it gets cleaned up before archival
        final_msg = mailinglist.craft_response(message, list_name, 
                                               list_name + '@' + host)
        archive.enqueue(list_name, final_msg)
        return POSTING
    

@route_like(CONFIRMING_SUBSCRIBE)
def CONFIRMING_UNSUBSCRIBE(message, list_name=None, id_number=None, host=None):
    original = CONFIRM.verify(list_name, message['from'], id_number)

    if original:
        mailinglist.remove_subscriber(message['from'], list_name)

        msg = view.respond(locals(), 'mail/unsubscribed.msg',
                           From="noreply@%(host)s",
                           To=message['from'],
                           Subject="You are now unsubscribed from %(list_name)s.")
        relay.deliver(msg)

        CONFIRM.cancel(list_name, message['from'], id_number)

        return START
    else:
        logging.warning("Invalid unsubscribe confirm from %s", message['from'])
        return CONFIRMING_UNSUBSCRIBE


@route("(address)@(host)", address=".+")
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def BOUNCING(message, address=None, host=None):
    msg = view.respond(locals(), 'mail/we_have_disabled_you.msg',
                       From='unbounce@librelist.com',
                       To=message['from'],
                       Subject='You have bounced and are disabled.')
    relay.deliver(msg)
    return BOUNCING

���������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/handlers/bounce.py�������������������������������������������0000644�0000765�0000024�00000003535�11242535750�023072� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config.settings import relay, CONFIRM
from lamson.routing import route, Router, route_like
from lamson.bounce import bounce_to
from app.model import mailinglist, bounce
from app import handlers
from email.utils import parseaddr



def force_to_bounce_state(message):
    # set their admin module state to disabled
    name, address = parseaddr(message.bounce.final_recipient)
    Router.STATE_STORE.set_all(address, 'BOUNCING')
    Router.STATE_STORE.set('app.handlers.bounce', address, 'BOUNCING')
    mailinglist.disable_all_subscriptions(message.bounce.final_recipient)

@route(".+")
def BOUNCED_HARD(message):
    if mailinglist.find_subscriptions(message.bounce.final_recipient):
        force_to_bounce_state(message)

    bounce.archive_bounce(message)
    return handlers.admin.START

@route(".+")
def BOUNCED_SOFT(message):
    if mailinglist.find_subscriptions(message.bounce.final_recipient):
        force_to_bounce_state(message)
        msg = bounce.mail_to_you_is_bouncing(message)
        relay.deliver(msg)

    bounce.archive_bounce(message)
    return handlers.admin.START


@route('unbounce@(host)')
def BOUNCING(message, host=None):
    CONFIRM.send(relay, 'unbounce', message, 'mail/unbounce_confirm.msg',
                      locals())

    return CONFIRMING_UNBOUNCE


@route('unbounce-confirm-(id_number)@(host)')
def CONFIRMING_UNBOUNCE(message, id_number=None, host=None):
    original = CONFIRM.verify('unbounce', message['from'], id_number)

    if original:
        relay.deliver(bounce.you_are_now_unbounced(message))
        name, address = parseaddr(message['from'])
        Router.STATE_STORE.set_all(address, 'POSTING')
        mailinglist.enable_all_subscriptions(message['from'])
        return UNBOUNCED

@route('unbounce@(host)')
def UNBOUNCED(message, host=None):
    # we just ignore these since they may be strays
    return UNBOUNCED


�������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020543� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/__init__.py��������������������������������������������0000644�0000765�0000024�00000000000�11225417571�022637� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/archive.py���������������������������������������������0000644�0000765�0000024�00000005302�11250554757�022541� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import with_statement
from lamson import queue
from config import settings
from datetime import datetime
import os
import shutil
import simplejson as json
import base64
import stat

ALLOWED_HEADERS = set([
 "From", "In-Reply-To", "List-Id",
 "Precedence", "References", "Reply-To",
 "Return-Path", "Sender",
 "Subject", "To", "Message-Id",
 "Date", "List-Id",
])

DIR_MOD = stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH
FILE_MOD = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH

def day_of_year_path():
    return "%d/%0.2d/%0.2d" % datetime.today().timetuple()[0:3]

def store_path(list_name, name):
    datedir = os.path.join(settings.ARCHIVE_BASE, list_name, day_of_year_path())

    if not os.path.exists(datedir):
        os.makedirs(datedir)

    return os.path.join(datedir, name)

def fix_permissions(path):
    os.chmod(path, DIR_MOD)

    for root, dirs, files in os.walk(path):
        os.chmod(root, DIR_MOD)
        for f in files:
            os.chmod(os.path.join(root, f), FILE_MOD)

def update_json(list_name, key, message):
    jpath = store_path(list_name, 'json')
    json_file = key + ".json"
    json_archive = os.path.join(jpath, json_file)

    if not os.path.exists(jpath):
        os.makedirs(jpath)

    with open(json_archive, "w") as f:
        f.write(to_json(message.base))

    fix_permissions(jpath)


def enqueue(list_name, message):
    qpath = store_path(list_name, 'queue')
    pending = queue.Queue(qpath, safe=True)
    white_list_cleanse(message)

    key = pending.push(message)
    fix_permissions(qpath)

    update_json(list_name, key, message)
    return key

def white_list_cleanse(message):
    for key in message.keys():
        if key not in ALLOWED_HEADERS:
            del message[key]

    message['from'] = message['from'].replace(u'@',u'-AT-')
   

def json_encoding(base):
    ctype, ctp = base.content_encoding['Content-Type']
    cdisp, cdp = base.content_encoding['Content-Disposition']
    ctype = ctype or "text/plain"
    filename = ctp.get('name',None) or cdp.get('filename', None)

    if ctype.startswith('text') or ctype.startswith('message'):
        encoding = None
    else:
        encoding = "base64"

    return {'filename': filename, 'type': ctype, 'disposition': cdisp,
            'format': encoding}

def json_build(base):
    data = {'headers': base.headers,
                'body': base.body,
                'encoding': json_encoding(base),
                'parts': [json_build(p) for p in base.parts],
            }

    if data['encoding']['format'] and base.body:
        data['body'] = base64.b64encode(base.body)

    return data

def to_json(base):
    return json.dumps(json_build(base), sort_keys=True, indent=4)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/bounce.py����������������������������������������������0000644�0000765�0000024�00000002312�11230156215�022352� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import view, encoding, queue
from config import settings


def mail_to_you_is_bouncing(message):
    reason = message.bounce.error_for_humans()

    msg = view.respond(locals(), 'mail/you_bounced.msg',
                       From='unbounce@librelist.com',
                       To=message.bounce.original['to'],
                       Subject="Email to you is bouncing.")

    if message.bounce.report:
        for report in message.bounce.report:
            msg.attach('bounce_report.msg', content_type='text/plain', data=encoding.to_string(report),
                       disposition='attachment')

    if message.bounce.notification:
        msg.attach('notification_report.msg', content_type='text/plain',
                   data=encoding.to_string(message.bounce.notification),
                   disposition='attachment')

    return msg

def you_are_now_unbounced(message):
    msg = view.respond(locals(), 'mail/you_are_unbounced.msg',
                       From='noreply@librelist.com',
                       To=message['from'],
                       Subject="You are now unbounced.")

    return msg


def archive_bounce(message):
    qu = queue.Queue(settings.BOUNCE_ARCHIVE)
    qu.push(message)

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/confirmation.py����������������������������������������0000644�0000765�0000024�00000001763�11242535652�023611� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from webapp.librelist.models import Confirmation

class DjangoConfirmStorage():
    def clear(self):
        Confirmation.objects.all().delete()

    def get(self, target, from_address):
        confirmations = Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target)
        if confirmations:
            return confirmations[0].expected_secret, confirmations[0].pending_message_id
        else:
            return None, None

    def delete(self, target, from_address):
        Confirmation.objects.filter(from_address=from_address, 
                                                list_name=target).delete()

    def store(self, target, from_address, expected_secret, pending_message_id):
        conf = Confirmation(from_address=from_address,
                            expected_secret = expected_secret,
                            pending_message_id = pending_message_id,
                            list_name=target)
        conf.save()

�������������lamson-1.0pre11/examples/librelist/app/model/mailinglist.py�����������������������������������������0000644�0000765�0000024�00000010527�11250552523�023426� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from webapp.librelist.models import *
from django.db.models import Q
from email.utils import parseaddr
from lamson.mail import MailResponse
from config import settings
from lib import metaphone
import Stemmer

def stem_and_meta(list_name):
    s = Stemmer.Stemmer('english')
    name = " ".join(s.stemWords(list_name.split('.')))
    return metaphone.dm(name)

def create_list(list_name):
    list_name = list_name.lower()
    mlist = find_list(list_name)
    sim_pri, sim_sec = stem_and_meta(list_name)

    if not mlist:
        mlist = MailingList(archive_url = "/archives/" + list_name,
                            archive_queue = "/queues/" + list_name,
                            name=list_name,
                            similarity_pri = sim_pri,
                            similarity_sec = sim_sec)
        mlist.save()

    return mlist

def delete_list(list_name):
    MailingList.objects.filter(name = list_name).delete()

def find_list(list_name):
    mlists = MailingList.objects.filter(name = list_name)
    if mlists:
        return mlists[0]
    else:
        return None

def add_subscriber(address, list_name):
    mlist = create_list(list_name)
    sub_name, sub_addr = parseaddr(address)
    subs = find_subscriptions(address, list_name)

    if not subs:
        sub = Subscription(subscriber_name = sub_name,
                           subscriber_address = sub_addr,
                           mailing_list = mlist)
        sub.save()
        return sub
    else:
        return subs[0]

def remove_subscriber(address, list_name):
    find_subscriptions(address, list_name).delete()

def remove_all_subscriptions(address):
    find_subscriptions(address).delete()

def find_subscriptions(address, list_name=None):
    sub_name, sub_addr = parseaddr(address)

    if list_name:
        mlist = find_list(list_name)
    else:
        mlist = None

    if mlist:
        subs = Subscription.objects.filter(
            subscriber_address=sub_addr, mailing_list = mlist
        ).exclude(
            enabled=False)
    else:
        subs = Subscription.objects.filter(
            subscriber_address=sub_addr
        ).exclude(
            enabled=False)

    return subs


def post_message(relay, message, list_name, host):
    mlist = find_list(list_name)
    assert mlist, "User is somehow able to post to list %s" % list_name

    for sub in mlist.subscription_set.all().values('subscriber_address'):
        list_addr = "%s@%s" % (list_name, host)
        delivery = craft_response(message, list_name, list_addr) 
        relay.deliver(delivery, To=sub['subscriber_address'], From=list_addr)


def craft_response(message, list_name, list_addr):
    response = MailResponse(To=list_addr, 
                            From=message['from'],
                            Subject=message['subject'])

    msg_id = message['message-id']

    response.update({
        "Sender": list_addr, 
        "Reply-To": list_addr,
        "List-Id": list_addr,
        "List-Unsubscribe": "<mailto:%s-unsubscribe@librelist.com>" % list_name,
        "List-Archive": "<http://librelist.com/archives/%s/>" % list_name,
        "List-Post": "<mailto:%s>" % list_addr,
        "List-Help": "<http://librelist.com/help.html>",
        "List-Subscribe": "<mailto:%s-subscribe@librelist.com>" % list_name,
        "Return-Path": list_addr, 
        "Precedence": 'list',
    })

    if 'date' in message:
        response['Date'] = message['date']

    if 'references' in message:
        response['References'] = message['References']
    elif msg_id:
        response['References'] = msg_id

    if msg_id:
        response['message-id'] = msg_id

        if 'in-reply-to' not in message:
            response["In-Reply-To"] = message['Message-Id']

    if message.all_parts():
        response.attach_all_parts(message)
    else:
        response.Body = message.body()

    return response

def disable_all_subscriptions(address):
    Subscription.objects.filter(subscriber_address=address).update(enabled=False)

def enable_all_subscriptions(address):
    Subscription.objects.filter(subscriber_address=address).update(enabled=True)

def similar_named_lists(list_name):
    sim_pri, sim_sec = stem_and_meta(list_name)
    sim_sec = sim_sec or sim_pri

    return MailingList.objects.filter(Q(similarity_pri = sim_pri) | 
                                       Q(similarity_sec =
                                         sim_sec))

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/model/state_storage.py���������������������������������������0000644�0000765�0000024�00000003403�11233527122�023747� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson.routing import StateStorage, ROUTE_FIRST_STATE
from webapp.librelist.models import UserState

class UserStateStorage(StateStorage):

    def clear(self):
        for state in UserState.objects.all():
            state.delete()

    def _find_state(self, key, sender):
        states = UserState.objects.filter(state_key = key,
                                          from_address = sender)
        if states:
            return states[0]
        else:
            return None

    def get(self, key, sender):
        stored_state = self._find_state(key, sender)
        if stored_state:
            return stored_state.state
        else:
            return ROUTE_FIRST_STATE

    def key(self, key, sender):
        raise Exception("THIS METHOD MEANS NOTHING TO DJANGO!")

    def set(self, key, sender, to_state):
        stored_state = self._find_state(key, sender)

        if stored_state:
            if to_state == "START":
                # don't store these, they're the default when it doesn't exist
                stored_state.delete()

            stored_state.state = to_state
            stored_state.save()
        else:
            # avoid storing start states
            if to_state != "START":
                stored_state = UserState(state_key = key, from_address = sender,
                                         state=to_state)
                stored_state.save()

    def set_all(self, sender, to_state):
        """
        This isn't part of normal lamson code, it's used to 
        control the states for all of the app.handlers.admin
        lists during a bounce.
        """
        stored_states = UserState.objects.filter(from_address = sender)

        for stored in stored_states:
            stored.state = to_state
            stored.save()


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/���������������������������������������������������0000755�0000765�0000024�00000000000�11313464573�021440� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022363� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/bad_list_name.msg�����������������������������0000644�0000765�0000024�00000001374�11233143550�025646� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

Sorry to tell you, but the list name you gave:

{{ bad_list }}@{{ host }}

is not a valid name for a list at librelist.com.

You probably put a '-' character in it, but that character is used by
librelist to separate list names from commands.


WHAT IS VALID

List names can only have characters, numbers, and the '.' (period)
character.  You can't have any other special characters.


{% if similar_lists -%}
SIMILAR LISTS

Maybe you got the name wrong, so make sure you didn't mean one of these
similarly named lists:

{% for possible in similar_lists[0:10] -%}
* {{ possible.name }}
{% endfor %}

{% endif -%}

TRYING AGAIN

Simply just try again with a valid list name and we'll get to work on
making the list you want.


Thank you,

      librelist.com

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/confirmation.msg������������������������������0000644�0000765�0000024�00000001070�11231523356�025552� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

You (or someone pretending to be you) has asked to {{action}}
{{list_name}}, but before I can do that I need you to confirm that you
really want to do this.  I know, I know, you hate having to confirm
everything you do, but if I don't confirm your request, then someone who
hates you can forge your identity.

We wouldn't want that now would we?  Good!


CONFIRM BY REPLYING

If you really did mean {{ list_name }}, then reply to this message
and I'll do it right away.  I'll send you one more message telling you
when I'm done.

Thank You,

      librelist.com

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/create_confirmation.msg�����������������������0000644�0000765�0000024�00000001226�11231523400�027066� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

It looks like the list "{{ list_name }}" doesn't exist,
so let's figure out what you meant to do.

{% if similar_lists -%}
First, maybe you got the name wrong, so make sure you didn't mean one of
these similarly named lists:

{% for possible in similar_lists[0:10] -%}
* {{ possible.name }}
{% endfor -%}

{% else -%}
First, it looks like there aren't any lists similar to "{{list_name}}".
{% endif %}


CONFIRM BY REPLYING

If you really do want to make a list named "{{ list_name }}" then all
you have to do is reply to this email and we'll make the list, subscribe
you, and send your original email as the first email.


Thank You,

      librelist.com

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/subscribed.msg��������������������������������0000644�0000765�0000024�00000001106�11242573343�025212� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

You are now subscribed to the {{list_name}} mailing list, and your
original email has NOT been forwarded to the mailing list.


YOUR FIRST MAIL WAS DROPPED

Sorry about that, but you'll have to resend that email now that
you're subscribed.


UNSUBSCRIBING

Now that you're subscribed to {{list_name}} you probably want to
remember a few email addresses you can use to manage your subscription:

* {{list_name}}-unsubscribe@{{host}}
  - Send to this address to unsubscribe from the list.

That's all there is to it.  Enjoy, and be nice please.


Thank You,

      librelist.com
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/unbounce_confirm.msg��������������������������0000644�0000765�0000024�00000000450�11227711066�026420� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

Well it looks like your email was bouncing, and now you'd like to have
your account reinstated.  Alright, hopefully you've fixed everything and
your email will keep working.

To get everything turned back on, just reply to this email and we'll
reset it.

Thanks You,

       librelist.com


������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/unsubscribed.msg������������������������������0000644�0000765�0000024�00000000713�11231523547�025557� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

You are successfully unsubscribed from {{list_name}} and won't receive
any more emails from them.  This means that, if this list archives were
closed, then you won't have access anymore.  


GETTING BACK ON

If you want to subscribe again then just send an email to:

{{list_name}}-subscribe@{{host}}

You'll have to confirm again, but that is all.

Well, we're sad to see you go, and hope you enjoyed using the service.

Thank You,

      librelist.com
�����������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/we_have_disabled_you.msg����������������������0000644�0000765�0000024�00000001246�11231523621�027223� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

We tried contacting you about this, but your email account bounced
recently.  That was a while ago so we can't say why or how it happened,
but it did, and we had to turn off your subscriptions temporarily until
you dealt with it.

GETTING UNBOUNCED

Now that you've sent an email to {{ address }}@{{ host }} we're assuming
everything is aright, so here's what you need to do if you want to turn
everything back on:

Reply to this email and then we'll confirm that your account is working
and you'll be turned back on.

Sorry about this, but it had to be done so everyone else could enjoy
librelist.com without getting your bounce messages.

Thank You,

      librelist.com
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/you_are_unbounced.msg�������������������������0000644�0000765�0000024�00000002131�11230147421�026560� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

Alright, looks like your account is working now.
You managed to reply to an unbounce confirmation message
so that means you can at least receive and send again.


RESEND YOUR ORIGINAL EMAIL

We had to drop your original email in order to handle
checking that your email worked, so please resend it.


AVOIDING BOUNCES IN THE FUTURE

Please make sure that whatever caused the bounce doesn't
happen again.  Here's some thing you'll want to disable
since they actually are very bad ideas you don't need
anymore:

1) Auto-away messages.  These always mess with mailing lists and are 
considered very rude, even if they work right.

2) Spam filters that send back spam results as bounces.
If your spam product works this way, please get a new spam
product.  It is both a violation of various RFCs and it is
a security hole since you are releasing information that
spammers can use to get around your filters.

3) Overly aggressive graylist systems may also return error 
messages that would cause you to bounce.  Please put your
subscriptions into your whitelist.

Thanks for your patience,

       librelist.com
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/app/templates/mail/you_bounced.msg�������������������������������0000644�0000765�0000024�00000001745�11231524425�025404� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

It looks like your email account is bouncing, so you may not even get
this.  Your email server gave me some reasons, which I've attached
to this messages, as well as reporting this diagnostic code:

{{ reason }}

Hopefully that all means something to you and you can fix it.


ACCOUNT DISABLED

We're going to have to disable your account on all mailing lists until
you fix this problem and can receive mail.  When you fix it, try to fix
the problem permanently so that you don't continue to get disabled.


GETTING UNBOUNCED

To enable your subscriptions again, simply reply to this email and we'll
turn everything back on.  You *will* miss previously sent messages and
none of your messages you sent will be delivered until you reinstate
yourself.


GONE IN 30 DAYS

If your account stays disabled for more than 30 days then it will be
deleted.  Sorry about that, but we have to keep things sane or it will
impact the other people who use librelist.com.

Thanks You,

       librelist.com

���������������������������lamson-1.0pre11/examples/librelist/config/����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020130� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/config/__init__.py�����������������������������������������������0000644�0000765�0000024�00000000000�11225417542�022222� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/config/boot.py���������������������������������������������������0000644�0000765�0000024�00000001715�11227341131�021434� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson import view
import logging
import logging.config
import jinja2
from app.model import state_storage

logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.LOG_EXCEPTIONS=True
Router.STATE_STORE=state_storage.UserStateStorage()

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

���������������������������������������������������lamson-1.0pre11/examples/librelist/config/logging.conf����������������������������������������������0000644�0000765�0000024�00000001064�11225417542�022421� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=fileHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[logger_routing]
level=DEBUG
handlers=fileHandler
qualname=routing
propagate=0

[handler_fileHandler]
# this works using FileHandler
class=FileHandler
# If you have Python2.6 you can use this and it will work when you use logrotate
#class=WatchedFileHandler
level=DEBUG
formatter=defaultFormatter
args=("logs/lamson.log",)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/config/settings.py�����������������������������������������������0000644�0000765�0000024�00000001736�11251075243�022341� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file contains python variables that configure Lamson for email processing.
import logging
import os
from lamson import confirm, encoding


encoding.VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v or '-AT-' in v


os.environ['DJANGO_SETTINGS_MODULE'] = 'webapp.settings'

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

handlers = ['app.handlers.bounce', 'app.handlers.admin']

router_defaults = {
    'host': 'librelist\\.com',
    'list_name': '[a-zA-Z0-9\.]+',
    'id_number': '[a-z0-9]+',
}

template_config = {'dir': 'app', 'module': 'templates'}

# the config/boot.py will turn these values into variables set in settings

PENDING_QUEUE = "run/pending"
ARCHIVE_BASE = "app/data/archive"
BOUNCE_ARCHIVE = "run/bounces"

SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}

from app.model.confirmation import DjangoConfirmStorage
CONFIRM = confirm.ConfirmationEngine('run/pending', DjangoConfirmStorage())

����������������������������������lamson-1.0pre11/examples/librelist/config/test_logging.conf�����������������������������������������0000644�0000765�0000024�00000001042�11225721404�023447� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=stdoutHandler,stderrHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=stdoutHandler

[logger_routing]
level=DEBUG
handlers=stderrHandler
qualname=routing
propagate=0

[handler_stdoutHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)

[handler_stderrHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stderr,)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/config/testing.py������������������������������������������������0000644�0000765�0000024�00000002234�11227341200�022140� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson import view
from lamson.routing import Router
from lamson.server import Relay
import jinja2
import logging
import logging.config
import os
from app.model import state_storage

logging.config.fileConfig("config/test_logging.conf")

# the relay host to actually send the final message to (set debug=1 to see what
# the relay is saying to the log server).
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=0)


settings.receiver = None

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.LOG_EXCEPTIONS=False
Router.STATE_STORE=state_storage.UserStateStorage()


view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

# if you have pyenchant and enchant installed then the template tests will do
# spell checking for you, but you need to tell pyenchant where to find itself
# if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
#     os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020157� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/backup����������������������������������������������������0000644�0000765�0000024�00000000132�11232764535�021343� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup

backup $TARGET $BACKUP

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/env/������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020747� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/env/testing�����������������������������������������������0000644�0000765�0000024�00000001315�11233004154�022330� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# An deploy/env file contains the settings and special functions
# for that environment.  


LOG4SH_CONFIGURATION=deploy/log4sh.properties
SOURCE=$HOME/projects/lamson/examples/librelist
TARGET=$HOME/deploy/testing
BACKUP=$HOME/deploy/backup/testing
FAILS=$HOME/deploy/fails/testing


function stop {
    logger_info "Stopping testing deployment."
    pushd $1
    assert_in $1

    lamson stop -ALL run
    popd
}


function start {
    logger_info "Starting testing deployment."
    pushd $1
    assert_in $1

    lamson start
    popd
}


function setup {
    assert_in $SOURCE

    mkdir -p $SOURCE
    mkdir -p $TARGET
    mkdir -p $BACKUP
    mkdir -p $FAILS

    rm -rf run/*
    rm -rf app/data/archive/*
}


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/forward���������������������������������������������������0000644�0000765�0000024�00000000250�11232766244�021542� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup

stop $TARGET

backup $TARGET $BACKUP

deploy_code $SOURCE $TARGET/..

migrate $TARGET

start $TARGET

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/lib/������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020725� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/lib/log4sh������������������������������������������������0000644�0000765�0000024�00000335345�11232747060�022057� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# $Id: log4sh 574 2007-06-02 20:09:15Z sfsetse $
# vim:syntax=sh:sts=2
# vim:foldmethod=marker:foldmarker=/**,*/
#
#/**
# <?xml version="1.0" encoding="UTF-8"?>
# <s:shelldoc xmlns:s="http://www.forestent.com/2005/XSL/ShellDoc">
# <s:header>
# log4sh 1.4.2
#
# http://log4sh.sourceforge.net/
#
# written by Kate Ward &lt;kate.ward@forestent.com>
# released under the LGPL
#
# this module implements something like the log4j module from the Apache group
#
# notes:
# *) the default appender is a ConsoleAppender named stdout with a level
#    of ERROR and layout of SimpleLayout
# *) the appender levels are as follows (decreasing order of output):
#    TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
# </s:header>
#*/

# shell flags for log4sh:
# u - treat unset variables as an error when performing parameter expansion
__LOG4SH_SHELL_FLAGS='u'

# save the current set of shell flags, and then set some for log4sh
__log4sh_oldShellFlags=$-
for _log4sh_shellFlag in `echo "${__LOG4SH_SHELL_FLAGS}" |sed 's/\(.\)/\1 /g'`
do
  set -${_log4sh_shellFlag}
done

#
# constants
#
__LOG4SH_VERSION='1.4.2'

__LOG4SH_TRUE=0
__LOG4SH_FALSE=1
__LOG4SH_ERROR=2
__LOG4SH_NULL='~'

__LOG4SH_APPENDER_FUNC_PREFIX='_log4sh_app_'
__LOG4SH_APPENDER_INCLUDE_EXT='.inc'

__LOG4SH_TYPE_CONSOLE='ConsoleAppender'
__LOG4SH_TYPE_DAILY_ROLLING_FILE='DailyRollingFileAppender'
__LOG4SH_TYPE_FILE='FileAppender'
__LOG4SH_TYPE_ROLLING_FILE='RollingFileAppender'
__LOG4SH_TYPE_ROLLING_FILE_MAX_BACKUP_INDEX=1
__LOG4SH_TYPE_ROLLING_FILE_MAX_FILE_SIZE=10485760
__LOG4SH_TYPE_SMTP='SMTPAppender'
__LOG4SH_TYPE_SYSLOG='SyslogAppender'
__LOG4SH_TYPE_SYSLOG_FACILITY_NAMES=' kern user mail daemon auth security syslog lpr news uucp cron authpriv ftp local0 local1 local2 local3 local4 local5 local6 local7 '
__LOG4SH_TYPE_SYSLOG_FACILITY='user'

__LOG4SH_LAYOUT_HTML='HTMLLayout'
__LOG4SH_LAYOUT_SIMPLE='SimpleLayout'
__LOG4SH_LAYOUT_PATTERN='PatternLayout'

__LOG4SH_LEVEL_TRACE=0
__LOG4SH_LEVEL_TRACE_STR='TRACE'
__LOG4SH_LEVEL_DEBUG=1
__LOG4SH_LEVEL_DEBUG_STR='DEBUG'
__LOG4SH_LEVEL_INFO=2
__LOG4SH_LEVEL_INFO_STR='INFO'
__LOG4SH_LEVEL_WARN=3
__LOG4SH_LEVEL_WARN_STR='WARN'
__LOG4SH_LEVEL_ERROR=4
__LOG4SH_LEVEL_ERROR_STR='ERROR'
__LOG4SH_LEVEL_FATAL=5
__LOG4SH_LEVEL_FATAL_STR='FATAL'
__LOG4SH_LEVEL_OFF=6
__LOG4SH_LEVEL_OFF_STR='OFF'
__LOG4SH_LEVEL_CLOSED=255
__LOG4SH_LEVEL_CLOSED_STR='CLOSED'

__LOG4SH_PATTERN_DEFAULT='%d %p - %m%n'
__LOG4SH_THREAD_DEFAULT='main'

__LOG4SH_CONFIGURATION="${LOG4SH_CONFIGURATION:-log4sh.properties}"
__LOG4SH_CONFIG_PREFIX="${LOG4SH_CONFIG_PREFIX:-log4sh}"
__LOG4SH_CONFIG_LOG4J_CP='org.apache.log4j'

# the following IFS is *supposed* to be on two lines!!
__LOG4SH_IFS_ARRAY="
"
__LOG4SH_IFS_DEFAULT=' '

__LOG4SH_SECONDS=`eval "expr \`date '+%H \* 3600 + %M \* 60 + %S'\`"`

# configure log4sh debugging. set the LOG4SH_INFO environment variable to any
# non-empty value to enable info output, LOG4SH_DEBUG enable debug output, or
# LOG4SH_TRACE to enable trace output. log4sh ERROR and above messages are
# always printed. to send the debug output to a file, set the LOG4SH_DEBUG_FILE
# with the filename you want debug output to be written to.
__LOG4SH_TRACE=${LOG4SH_TRACE:+'_log4sh_trace '}
__LOG4SH_TRACE=${__LOG4SH_TRACE:-':'}
[ -n "${LOG4SH_TRACE:-}" ] && LOG4SH_DEBUG=1
__LOG4SH_DEBUG=${LOG4SH_DEBUG:+'_log4sh_debug '}
__LOG4SH_DEBUG=${__LOG4SH_DEBUG:-':'}
[ -n "${LOG4SH_DEBUG:-}" ] && LOG4SH_INFO=1
__LOG4SH_INFO=${LOG4SH_INFO:+'_log4sh_info '}
__LOG4SH_INFO=${__LOG4SH_INFO:-':'}

# set the constants to readonly
for _log4sh_const in `set |grep "^__LOG4SH_" |cut -d= -f1`; do
  readonly ${_log4sh_const}
done
unset _log4sh_const

#
# internal variables
#

__log4sh_filename=`basename $0`
__log4sh_tmpDir=''
__log4sh_trapsFile=''

__log4sh_alternative_mail='mail'

__log4sh_threadName=${__LOG4SH_THREAD_DEFAULT}
__log4sh_threadStack=${__LOG4SH_THREAD_DEFAULT}

__log4sh_seconds=0
__log4sh_secondsLast=0
__log4sh_secondsWrap=0

# workarounds for various commands
__log4sh_wa_strictBehavior=${__LOG4SH_FALSE}
(
  # determine if the set builtin needs to be evaluated. if the string is parsed
  # into two separate strings (common in ksh), then set needs to be evaled.
  str='x{1,2}'
  set -- ${str}
  test ! "$1" = 'x1' -a ! "${2:-}" = 'x2'
)
__log4sh_wa_setNeedsEval=$?


#=============================================================================
# Log4sh
#

#-----------------------------------------------------------------------------
# internal debugging
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_log</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_log()
{
  _ll__level=$1
  shift
  if [ -z "${LOG4SH_DEBUG_FILE:-}" ]; then
    echo "log4sh:${_ll__level} $@" >&2
  else
    echo "${_ll__level} $@" >>${LOG4SH_DEBUG_FILE}
  fi
  unset _ll__level
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_trace</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_trace()
{
  _log4sh_log "${__LOG4SH_LEVEL_TRACE_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
 }

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_debug</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_debug()
{
  _log4sh_log "${__LOG4SH_LEVEL_DEBUG_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_info</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_info()
{
  _log4sh_log "${__LOG4SH_LEVEL_INFO_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_warn</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_warn()
{
  echo "log4sh:${__LOG4SH_LEVEL_WARN_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_WARN_STR}" "$@"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_error</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_error()
{
  echo "log4sh:${__LOG4SH_LEVEL_ERROR_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_ERROR_STR}" "$@"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_fatal</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_fatal()
{
  echo "log4sh:${__LOG4SH_LEVEL_FATAL_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_FATAL_STR}" "$@"
}

#-----------------------------------------------------------------------------
# miscellaneous
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_mktempDir</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Creates a secure temporary directory within which temporary files can be
#     created. Honors the <code>TMPDIR</code> environment variable if it is
#     set.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>tmpDir=`_log4sh_mktempDir`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_mktempDir()
{
  _lmd_tmpPrefix='log4sh'

  # try the standard mktemp function
  ( exec mktemp -dqt ${_lmd_tmpPrefix}.XXXXXX 2>/dev/null ) && return

  # the standard mktemp didn't work. doing our own.
  if [ -n "${RANDOM:-}" ]; then
    # $RANDOM works
    _lmd_random=${RANDOM}${RANDOM}${RANDOM}$$
  elif [ -r '/dev/urandom' ]; then
    _lmd_random=`od -vAn -N4 -tu4 </dev/urandom |sed 's/^[^0-9]*//'`
  else
    # $RANDOM doesn't work
    _lmd_date=`date '+%Y%m%d%H%M%S'`
    _lmd_random=`expr ${_lmd_date} / $$`
    unset _lmd_date
  fi

  _lmd_tmpDir="${TMPDIR:-/tmp}/${_lmd_tmpPrefix}.${_lmd_random}"
  ( umask 077 && mkdir "${_lmd_tmpDir}" ) || {
    _log4sh_fatal 'could not create temporary directory! exiting'
    exit 1
  }

  ${__LOG4SH_DEBUG} "created temporary directory (${_lmd_tmpDir})"
  echo "${_lmd_tmpDir}"
  unset _lmd_random _lmd_tmpDir _lmd_tmpPrefix
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_updateSeconds</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the <code>__log4sh_seconds</code> variable to the number of seconds
#     elapsed since the start of the script.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_updateSeconds`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_updateSeconds()
{
  if [ -n "${SECONDS:-}" ]; then
    __log4sh_seconds=${SECONDS}
  else
    _lgs__date=`date '+%H \* 3600 + %M \* 60 + %S'`
    _lgs__seconds=`eval "expr ${_lgs__date} + ${__log4sh_secondsWrap} \* 86400"`
    if [ ${_lgs__seconds} -lt ${__log4sh_secondsLast} ]; then
      __log4sh_secondsWrap=`expr ${__log4sh_secondsWrap} + 1`
      _lgs__seconds=`expr ${_lgs_seconds} + 86400`
    fi
    __log4sh_seconds=`expr ${_lgs__seconds} - ${__LOG4SH_SECONDS}`
    __log4sh_secondsLast=${__log4sh_seconds}
    unset _lgs__date _lgs__seconds
  fi
}

#/**
# <s:function group="Log4sh" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_enableStrictBehavior</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Enables strict log4j behavior.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_enableStrictBehavior</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_enableStrictBehavior()
{
  __log4sh_wa_strictBehavior=${__LOG4SH_TRUE}
}

#/**
# <s:function group="Log4sh" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_setAlternative</function></funcdef>
#       <paramdef>string <parameter>command</parameter></paramdef>
#       <paramdef>string <parameter>path</parameter></paramdef>
#       <paramdef>boolean <parameter>useRuntimePath</parameter> (optional)</paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Specifies an alternative path for a command.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_setAlternative nc /bin/nc</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_setAlternative()
{
  if [ $# -lt 2 ]; then
    _log4sh_error 'log4sh_setAlternative(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  lsa_cmdName=$1
  lsa_cmdPath=$2
  lsa_useRuntimePath=${3:-}
  __log4sh_return=${__LOG4SH_TRUE}

  # check that the alternative command exists and is executable
  if [ \
    ! -x "${lsa_cmdPath}" \
    -a ${lsa_useRuntimePath:-${__LOG4SH_FALSE}} -eq ${__LOG4SH_FALSE} \
  ]; then
    # the alternative command is not executable
    _log4sh_error "log4sh_setAlternative(): ${lsa_cmdName}: command not found"
    __log4sh_return=${__LOG4SH_ERROR}
  fi

  # check for valid alternative
  if [ ${__log4sh_return} -eq ${__LOG4SH_TRUE} ]; then
    case ${lsa_cmdName} in
      mail) ;;
      nc)
        lsa_cmdVers=`${lsa_cmdPath} --version 2>&1 |head -1`
        if echo "${lsa_cmdVers}" |grep '^netcat' >/dev/null; then
          # GNU Netcat
          __log4sh_alternative_nc_opts='-c'
        else
          # older netcat (v1.10)
          if nc -q 0 2>&1 |grep '^no destination$' >/dev/null 2>&1; then
            # supports -q option
            __log4sh_alternative_nc_opts='-q 0'
          else
            # doesn't support the -q option
            __log4sh_alternative_nc_opts=''
          fi
        fi
        unset lsa_cmdVers
        ;;
      *)
        # the alternative is not valid
        _log4sh_error "unrecognized command alternative '${lsa_cmdName}'"
        __log4sh_return=${__LOG4SH_FALSE}
        ;;
    esac
  fi

  # set the alternative
  if [ ${__log4sh_return} -eq ${__LOG4SH_TRUE} ]; then
    eval __log4sh_alternative_${lsa_cmdName}="\${lsa_cmdPath}"
    ${__LOG4SH_DEBUG} "alternative '${lsa_cmdName}' command set to '${lsa_cmdPath}'"
  fi

  unset lsa_cmdName lsa_cmdPath
  return ${__log4sh_return}
}

#-----------------------------------------------------------------------------
# array handling
#
# note: arrays are '1' based
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_findArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Find the position of element in an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>
#       pos=`_log4sh_findArrayElement "$array" $element`
#     </funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_findArrayElement()
{
  __pos=`echo "$1" |awk '$0==e{print NR}' e="$2"`
  [ -n "${__pos}" ] && echo "${__pos}" || echo 0
  unset __pos
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>integer <parameter>position</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Retrieve the element at the given position from an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>element=`_log4sh_getArrayElement "$array" $position`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getArrayElement()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  _lgae_array=$1
  _lgae_index=$2
  ${__LOG4SH_TRACE} "_lgae_array='${_lgae_array}' _lgae_index='${_lgae_index}'"

  _lgae_oldIFS=${IFS} IFS=${__LOG4SH_IFS_ARRAY}
  if [ ${__log4sh_wa_setNeedsEval} -eq 0 ]; then
    set -- junk ${_lgae_array}
  else
    eval "set -- junk \"${_lgae_array}\""
    _lgae_arraySize=$#

    if [ ${_lgae_arraySize} -le ${__log4shAppenderCount} ]; then
      # the evaled set *didn't* work; failing back to original set command and
      # disabling the work around. (pdksh)
      __log4sh_wa_setNeedsEval=${__LOG4SH_FALSE}
      set -- junk ${_lgae_array}
    fi
  fi
  IFS=${_lgae_oldIFS}

  shift ${_lgae_index}
  ${__LOG4SH_TRACE} "1='${1:-}' 2='${2:-}' 3='${3:-}' ..."
  echo "$1"

  unset _lgae_array _lgae_arraySize _lgae_index _lgae_oldIFS
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry align="left">
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getArrayLength</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the length of an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>length=`_log4sh_getArrayLength "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getArrayLength()
{
  _oldIFS=${IFS} IFS=${__LOG4SH_IFS_ARRAY}
  set -- $1
  IFS=${_oldIFS} unset _oldIFS
  echo $#
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string[]</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_setArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>integer <parameter>position</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Place an element at a given location in an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_setArrayElement "$array" $position $element`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_setArrayElement()
{
  echo "$1" |awk '{if(NR==r){print e}else{print $0}}' r=$2 e="$3"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_peekStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Return the topmost element on a stack without removing the
#   element.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>element=`_log4sh_peekStack "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_peekStack()
{
  echo "$@" |awk '{line=$0}END{print line}'
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string[]</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_popStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Remove the top-most element from a stack. This command takes a
#   normal log4sh string array as input, but treats it as though it were a
#   stack.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_popStack "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_popStack()
{
  _array=$1
  _length=`_log4sh_getArrayLength "${_array}"`
  echo "${_array}" |awk '{if(NR<r){print $0}}' r=${_length}
  unset _array _length
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_pushStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Add a new element to the top of a stack. This command takes a normal
#   log4sh string array as input, but treats it as though it were a
#   stack.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_pushStack "$array" $element`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_pushStack()
{
  echo "${1:+$1${__LOG4SH_IFS_ARRAY}}$2"
}

#=============================================================================
# Appender
#

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_activateOptions</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Activate an appender's configuration. This should be called after
#     reconfiguring an appender via code. It needs only to be called once
#     before any logging statements are called. This calling of this function
#     will be required in log4sh 1.4.x.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_activateAppender myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_activateOptions()
{
  _aao_appender=$1
  ${__LOG4SH_APPENDER_FUNC_PREFIX}${_aao_appender}_activateOptions
  unset _aao_appender
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_close</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Disable any further logging via an appender. Once closed, the
#   appender can be reopened by setting it to any logging Level (e.g.
#   INFO).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_close myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_close()
{
  appender_setLevel $1 ${__LOG4SH_LEVEL_CLOSED_STR}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_exists</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Checks for the existance of a named appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>exists=`appender_exists myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_exists()
{
  _ae_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  [ "${_ae_index}" -gt 0 ] \
    && _ae_return=${__LOG4SH_TRUE} \
    || _ae_return=${__LOG4SH_FALSE}
  unset _ae_index
  return ${_ae_return}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getLayout</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Layout of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getLayout myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getLayout()
{
  _agl_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  _log4sh_getArrayElement "${__log4shAppenderLayouts}" ${_agl_index}
  unset _agl_index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setLayout</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>layout</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Layout of an Appender (e.g. PatternLayout)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setLayout myAppender PatternLayout</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setLayout()
{
  _asl_appender=$1
  _asl_layout=$2

  case ${_asl_layout} in
    ${__LOG4SH_LAYOUT_HTML}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_HTML})
      _asl_layout=${__LOG4SH_LAYOUT_HTML}
      ;;

    ${__LOG4SH_LAYOUT_SIMPLE}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_SIMPLE})
      _asl_layout=${__LOG4SH_LAYOUT_SIMPLE}
      ;;

    ${__LOG4SH_LAYOUT_PATTERN}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_PATTERN})
      _asl_layout=${__LOG4SH_LAYOUT_PATTERN}
      ;;

    *)
      _log4sh_error "unknown layout: ${_asl_layout}"
      return ${__LOG4SH_FALSE}
      ;;
  esac

  _asl_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  __log4shAppenderLayouts=`_log4sh_setArrayElement \
      "${__log4shAppenderLayouts}" ${_asl_index} "${_asl_layout}"`

  # resource the appender
  _appender_cache ${_asl_appender}

  unset _asl_appender _asl_index _asl_layout
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getLayoutByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Layout of an Appender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getLayoutByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getLayoutByIndex()
{
  _log4sh_getArrayElement "${__log4shAppenderLayouts}" $1
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getLevel</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current logging Level of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getLevel myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getLevel()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_getLevel(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  agl_appender=$1

  agl_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${agl_appender}`
  # TODO: put check for valid index here
  agl_level=`_log4sh_getArrayElement \
      "${__log4shAppenderLevels}" ${agl_index}`
  __log4sh_return=$?

  echo "${agl_level}"

  unset agl_appender agl_index agl_level
  return ${__log4sh_return}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setLevel</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Level of an Appender (e.g. INFO)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setLevel myAppender INFO</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setLevel()
{
  asl_appender=$1
  asl_level=$2

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asl_appender}`
  __log4shAppenderLevels=`_log4sh_setArrayElement \
    "${__log4shAppenderLevels}" ${_index} "${asl_level}"`

  # resource the appender
  _appender_cache ${asl_appender}

  unset asl_appender asl_level _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getLevelByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current logging Level of an Appender at the given array
#   index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getLevelByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getLevelByIndex()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  _log4sh_getArrayElement "${__log4shAppenderLevels}" $1
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Pattern of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>pattern=`appender_getPattern myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getPattern()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppenderPatterns" $_index
  unset _index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Pattern of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setPattern myAppender '%d %p - %m%n'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setPattern()
{
  asp_appender=$1
  asp_pattern=$2

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asp_appender}`
  __log4shAppenderPatterns=`_log4sh_setArrayElement \
    "${__log4shAppenderPatterns}" ${_index} "${asp_pattern}"`

  # resource the appender
  _appender_cache ${asp_appender}

  unset asp_appender asp_pattern _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getPatternByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Pattern of an Appender at the specified array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>pattern=`_appender_getPatternByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getPatternByIndex()
{
  _log4sh_getArrayElement "$__log4shAppenderPatterns" $1
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_parsePattern</function></funcdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#       <paramdef>string <parameter>priority</parameter></paramdef>
#       <paramdef>string <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Generate a logging message given a Pattern, priority, and message.
#   All dates will be represented as ISO 8601 dates (YYYY-MM-DD
#   HH:MM:SS).</para>
#   <para>Note: the '<code>%r</code>' character modifier does not work in the
#   Solaris <code>/bin/sh</code> shell</para>
#   <para>Example:
#     <blockquote>
#       <funcsynopsis>
#         <funcsynopsisinfo>_appender_parsePattern '%d %p - %m%n' INFO "message to log"</funcsynopsisinfo>
#       </funcsynopsis>
#     </blockquote>
#   </para>
# </entry>
# </s:function>
#*/
_appender_parsePattern()
{
  _pattern=$1
  _priority=$2
  _msg=$3

  _date=''
  _doEval=${__LOG4SH_FALSE}

  # determine if various commands must be run
  _oldIFS="${IFS}"; IFS='%'; set -- x${_pattern}; IFS="${_oldIFS}"
  if [ $# -gt 1 ]; then
    # run the date command??
    IFS='d'; set -- ${_pattern}x; IFS="${_oldIFS}"
    [ $# -gt 1 ] && _date=`date '+%Y-%m-%d %H:%M:%S'`

    # run the eval command?
    IFS='X'; set -- ${_pattern}x; IFS="${_oldIFS}"
    [ $# -gt 1 ] && _doEval=${__LOG4SH_TRUE}
  fi
  unset _oldIFS

  # escape any '\' and '&' chars in the message
  _msg=`echo "${_msg}" |sed 's/\\\\/\\\\\\\\/g;s/&/\\\\&/g'`

  # deal with any newlines in the message
  _msg=`echo "${_msg}" |tr '\n' ''`

  # parse the pattern
  _pattern=`echo "${_pattern}" |sed \
    -e 's/%c/shell/g' \
    -e 's/%d{[^}]*}/%d/g' -e "s/%d/${_date}/g" \
    -e "s/%F/${__log4sh_filename}/g" \
    -e 's/%L//g' \
    -e 's/%n//g' \
    -e "s/%-*[0-9]*p/${_priority}/g" \
    -e "s/%-*[0-9]*r/${__log4sh_seconds}/g" \
    -e "s/%t/${__log4sh_threadName}/g" \
    -e 's/%x//g' \
    -e 's/%X{/$\{/g' \
    -e 's/%%m/%%%m/g' -e 's/%%/%/g' \
    -e "s%m${_msg}" |tr '' '\n'`
  if [ ${_doEval} -eq ${__LOG4SH_FALSE} ]; then
    echo "${_pattern}"
  else
    eval "echo \"${_pattern}\""
  fi

  unset _date _doEval _msg _pattern _tag
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Type of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getType myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getType()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppenderTypes" $_index
  unset _index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getAppenderType</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Gets the Type of an Appender at the given array index
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getAppenderType 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getAppenderType()
{
  _appender_getTypeByIndex "$@"
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>type</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Type of an Appender (e.g. FileAppender)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setType myAppender FileAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setType()
{
  ast_appender=$1
  ast_type=$2

  # XXX need to verify types

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${ast_appender}`
  __log4shAppenderTypes=`_log4sh_setArrayElement \
    "${__log4shAppenderTypes}" ${_index} "${ast_type}"`

  # resource the appender
  _appender_cache ${ast_appender}

  unset ast_appender ast_type _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>type</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Sets the Type of an Appender (e.g. FileAppender)
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderType myAppender FileAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderType()
{
  appender_setType "$@"
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getTypeByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Type of an Appender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getTypeByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getTypeByIndex()
{
  _log4sh_getArrayElement "$__log4shAppenderTypes" $1
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_cache</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Dynamically creates an appender function in memory that will fully
#   instantiate itself when it is called.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_cache myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_cache()
{
  _ac__appender=$1

  _ac__inc="${__log4sh_tmpDir}/${_ac__appender}${__LOG4SH_APPENDER_INCLUDE_EXT}"

  cat >"${_ac__inc}" <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_ac__appender}_activateOptions()
{
  [ -n "\${FUNCNAME:-}" ] && \${__LOG4SH_TRACE} "\${FUNCNAME}()\${BASH_LINENO:+'(called from \${BASH_LINENO})'}"
  _appender_activate ${_ac__appender}
}

${__LOG4SH_APPENDER_FUNC_PREFIX}${_ac__appender}_append() { :; }
EOF

  # source the new functions
  . "${_ac__inc}"

  # call the activateOptions function
  # XXX will be removed in log4sh-1.5.x
  appender_activateOptions ${_ac__appender}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_activate</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Dynamically regenerates an appender function in memory that is fully
#     instantiated for a specific logging task.
#     </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_activate myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_activate()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  ${__LOG4SH_TRACE} "_appender_activate($#)"
  _aa_appender=$1
  ${__LOG4SH_TRACE} "_aa_appender='${_aa_appender}'"

  _aa_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${_aa_appender}`
  _aa_inc="${__log4sh_tmpDir}/${_aa_appender}${__LOG4SH_APPENDER_INCLUDE_EXT}"

  ### generate function for inclusion
  # TODO can we modularize this in the future?

  # send STDOUT to our include file
  exec 4>&1 >${_aa_inc}

  # header
  cat <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_aa_appender}_append()
{
  [ -n "\${FUNCNAME:-}" ] && \${__LOG4SH_TRACE} "\${FUNCNAME}()\${BASH_LINENO:+'(called from \${BASH_LINENO})'}"
  _la_level=\$1
  _la_message=\$2
EOF

  # determine the 'layout'
  _aa_layout=`_appender_getLayoutByIndex ${_aa_index}`
  ${__LOG4SH_TRACE} "_aa_layout='${_aa_layout}'"
  case ${_aa_layout} in
    ${__LOG4SH_LAYOUT_SIMPLE}|\
    ${__LOG4SH_LAYOUT_HTML})
      ${__LOG4SH_DEBUG} 'using simple/html layout'
      echo "  _la_layout=\"\${_la_level} - \${_la_message}\""
      ;;

    ${__LOG4SH_LAYOUT_PATTERN})
      ${__LOG4SH_DEBUG} 'using pattern layout'
      _aa_pattern=`_appender_getPatternByIndex ${_aa_index}`
      echo "  _la_layout=\`_appender_parsePattern '${_aa_pattern}' \${_la_level} \"\${_la_message}\"\`"
      ;;
  esac

  # what appender 'type' do we have? TODO check not missing
  _aa_type=`_appender_getTypeByIndex ${_aa_index}`
  ${__LOG4SH_TRACE} "_aa_type='${_aa_type}'"
  case ${_aa_type} in
    ${__LOG4SH_TYPE_CONSOLE})
      echo "  echo \"\${_la_layout}\""
      ;;

    ${__LOG4SH_TYPE_FILE}|\
    ${__LOG4SH_TYPE_ROLLING_FILE}|\
    ${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
      _aa_file=`_appender_file_getFileByIndex ${_aa_index}`
      ${__LOG4SH_TRACE} "_aa_file='${_aa_file}'"
      if [ "${_aa_file}" = 'STDERR' ]; then
        echo "  echo \"\${_la_layout}\" >&2"
      elif [ "${_aa_file}" != "${__LOG4SH_NULL}" ]; then
        # do rotation
        case ${_aa_type} in
          ${__LOG4SH_TYPE_ROLLING_FILE})
            # check whether the max file size has been exceeded
            _aa_rotIndex=`appender_file_getMaxBackupIndex ${_aa_appender}`
            _aa_rotSize=`appender_file_getMaxFileSize ${_aa_appender}`
            cat <<EOF
  _la_rotSize=${_aa_rotSize}
  _la_size=\`wc -c '${_aa_file}' |awk '{print \$1}'\`
  if [ \${_la_size} -ge \${_la_rotSize} ]; then
    if [ ${_aa_rotIndex} -gt 0 ]; then
      # rotate the appender file(s)
      _la_rotIndex=`expr ${_aa_rotIndex} - 1`
      _la_rotFile="${_aa_file}.\${_la_rotIndex}"
      [ -f "\${_la_rotFile}" ] && rm -f "\${_la_rotFile}"
      while [ \${_la_rotIndex} -gt 0 ]; do
        _la_rotFileLast="\${_la_rotFile}"
        _la_rotIndex=\`expr \${_la_rotIndex} - 1\`
        _la_rotFile="${_aa_file}.\${_la_rotIndex}"
        [ -f "\${_la_rotFile}" ] && mv -f "\${_la_rotFile}" "\${_la_rotFileLast}"
      done
      mv -f '${_aa_file}' "\${_la_rotFile}"
    else
      # keep no backups; truncate the file
      cp /dev/null "${_aa_file}"
    fi
    unset _la_rotFile _la_rotFileLast _la_rotIndex
  fi
  unset _la_rotSize _la_size
EOF
            ;;
          ${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
            ;;
        esac
        echo "  echo \"\${_la_layout}\" >>'${_aa_file}'"
      else
        # the file "${__LOG4SH_NULL}" is closed?? Why did we get here, and why
        # did I care when I wrote this bit of code?
        :
      fi

      unset _aa_file
      ;;

    ${__LOG4SH_TYPE_SMTP})
      _aa_smtpTo=`appender_smtp_getTo ${_aa_appender}`
      _aa_smtpSubject=`appender_smtp_getSubject ${_aa_appender}`

      cat <<EOF
  echo "\${_la_layout}" |\\
      ${__log4sh_alternative_mail} -s "${_aa_smtpSubject}" ${_aa_smtpTo}
EOF
      ;;

    ${__LOG4SH_TYPE_SYSLOG})
      cat <<EOF
  case "\${_la_level}" in
    ${__LOG4SH_LEVEL_TRACE_STR}) _la_tag='debug' ;;  # no 'trace' equivalent
    ${__LOG4SH_LEVEL_DEBUG_STR}) _la_tag='debug' ;;
    ${__LOG4SH_LEVEL_INFO_STR}) _la_tag='info' ;;
    ${__LOG4SH_LEVEL_WARN_STR}) _la_tag='warning' ;;  # 'warn' is deprecated
    ${__LOG4SH_LEVEL_ERROR_STR}) _la_tag='err' ;;     # 'error' is deprecated
    ${__LOG4SH_LEVEL_FATAL_STR}) _la_tag='alert' ;;
  esac
EOF

      _aa_facilityName=`appender_syslog_getFacility ${_aa_appender}`
      _aa_syslogHost=`appender_syslog_getHost ${_aa_appender}`
      _aa_hostname=`hostname |sed 's/^\([^.]*\)\..*/\1/'`

      # are we logging to a remote host?
      if [ -z "${_aa_syslogHost}" ]; then
        # no -- use logger
        cat <<EOF
  ( exec logger -p "${_aa_facilityName}.\${_la_tag}" \
      -t "${__log4sh_filename}[$$]" "\${_la_layout}" 2>/dev/null )
  unset _la_tag
EOF
      else
        # yes -- use netcat
        if [ -n "${__log4sh_alternative_nc:-}" ]; then
          case ${_aa_facilityName} in
            kern) _aa_facilityCode=0 ;;            # 0<<3
            user) _aa_facilityCode=8 ;;            # 1<<3
            mail) _aa_facilityCode=16 ;;           # 2<<3
            daemon) _aa_facilityCode=24 ;;         # 3<<3
            auth|security) _aa_facilityCode=32 ;;  # 4<<3
            syslog) _aa_facilityCode=40 ;;         # 5<<3
            lpr) _aa_facilityCode=48 ;;            # 6<<3
            news) _aa_facilityCode=56 ;;           # 7<<3
            uucp) _aa_facilityCode=64 ;;           # 8<<3
            cron) _aa_facilityCode=72 ;;           # 9<<3
            authpriv) _aa_facilityCode=80 ;;       # 10<<3
            ftp) _aa_facilityCode=88 ;;            # 11<<3
            local0) _aa_facilityCode=128 ;;        # 16<<3
            local1) _aa_facilityCode=136 ;;        # 17<<3
            local2) _aa_facilityCode=144 ;;        # 18<<3
            local3) _aa_facilityCode=152 ;;        # 19<<3
            local4) _aa_facilityCode=160 ;;        # 20<<3
            local5) _aa_facilityCode=168 ;;        # 21<<3
            local6) _aa_facilityCode=176 ;;        # 22<<3
            local7) _aa_facilityCode=184 ;;        # 23<<3
          esac

          cat <<EOF
  case \${_la_tag} in
    alert) _la_priority=1 ;;
    err|error) _la_priority=3 ;;
    warning|warn) _la_priority=4 ;;
    info) _la_priority=6 ;;
    debug) _la_priority=7 ;;
  esac
  _la_priority=\`expr ${_aa_facilityCode} + \${_la_priority}\`
  _la_date=\`date "+%b %d %H:%M:%S"\`
  _la_hostname='${_aa_hostname}'

  _la_syslogMsg="<\${_la_priority}>\${_la_date} \${_la_hostname} \${_la_layout}"

  # do RFC 3164 cleanups
  _la_date=\`echo \"\${_la_date}\" |sed 's/ 0\([0-9]\) /  \1 /'\`
  _la_syslogMsg=\`echo "\${_la_syslogMsg}" |cut -b1-1024\`

  ( echo "\${_la_syslogMsg}" |\
      exec ${__log4sh_alternative_nc} ${__log4sh_alternative_nc_opts} -w 1 -u \
          ${_aa_syslogHost} 514 )
  unset _la_tag _la_priority _la_date _la_hostname _la_syslogMsg
EOF
          unset _aa_facilityCode _aa_syslogHost _aa_hostname
        else
          # no netcat alternative set; doing nothing
          :
        fi
      fi
      unset _aa_facilityName
      ;;

    *) _log4sh_error "unrecognized appender type (${_aa_type})" ;;
  esac

  # footer
  cat <<EOF
  unset _la_level _la_message _la_layout
}
EOF

  # override the activateOptions function as we don't need it anymore
  cat <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_aa_appender}_activateOptions() { :; }
EOF

  # restore STDOUT
  exec 1>&4 4>&-

  # source the newly created function
  ${__LOG4SH_TRACE} 're-sourcing the newly created function'
  . "${_aa_inc}"

  unset _aa_appender _aa_inc _aa_layout _aa_pattern _aa_type
}

#-----------------------------------------------------------------------------
# FileAppender
#

#/**
# <s:function group="FileAppender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_file_getFileByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the filename of a FileAppender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_file_getFileByIndex 3</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_file_getFileByIndex()
{
  _log4sh_getArrayElement "${__log4shAppender_file_files}" $1
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the filename of a FileAppender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_getFile myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getFile()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppender_file_files" $_index
  unset _index
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the filename for a FileAppender (e.g. <filename>STDERR</filename> or
#     <filename>/var/log/log4sh.log</filename>).
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setFile myAppender STDERR</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setFile()
{
  afsf_appender=$1
  afsf_file=$2
  ${__LOG4SH_TRACE} "afsf_appender='${afsf_appender}' afsf_file='${afsf_file}'"

  if [ -n "${afsf_appender}" -a -n "${afsf_file}" ]; then
    # set the file
    _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${afsf_appender}`
    __log4shAppender_file_files=`_log4sh_setArrayElement \
      "${__log4shAppender_file_files}" ${_index} "${afsf_file}"`
    _return=$?

    # create the file (if it isn't already)
    if [ ${_return} -eq ${__LOG4SH_TRUE} \
      -a ! "${afsf_file}" '=' "${__LOG4SH_NULL}" \
      -a ! "${afsf_file}" '=' 'STDERR' \
      -a ! -f "${afsf_file}" \
    ]; then
      touch "${afsf_file}" 2>/dev/null
      _result=$?
      # determine success of touch command
      if [ ${_result} -eq 1 ]; then
        _log4sh_error "appender_file_setFile(): could not create file (${afsf_file}); closing appender"
        appender_setLevel ${afsf_appender} ${__LOG4SH_LEVEL_CLOSED_STR}
      fi
      unset _result
    fi
  else
    _log4sh_error 'appender_file_setFile(): missing appender and/or file'
    _return=${__LOG4SH_FALSE}
  fi

  # resource the appender
  _appender_cache ${afsf_appender}

  unset afsf_appender afsf_file _index
  return ${_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.2</emphasis></para>
#   <para>
#     Set the filename for a FileAppender (e.g. "STDERR" or
#     "/var/log/log4sh.log")
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderFile myAppender STDERR</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderFile()
{
  appender_file_setFile "$@"
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>integer</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getMaxBackupIndex</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Returns the value of the MaxBackupIndex option.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_getMaxBackupIndex myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getMaxBackupIndex()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_file_getMaxBackupIndex(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  afgmbi_appender=$1

  afgmbi_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afgmbi_appender}`
  # TODO: put check for valid index here
  _log4sh_getArrayElement \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" ${afgmbi_index}
  __log4sh_return=$?

  unset afgmbi_appender afgmbi_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setMaxBackupIndex</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the maximum number of backup files to keep around.</para>
#   <para>
#     The <emphasis role="strong">MaxBackupIndex</emphasis> option determines
#     how many backup files are kept before the oldest is erased. This option
#     takes a positive integer value. If set to zero, then there will be no
#     backup files and the log file will be truncated when it reaches
#     <option>MaxFileSize</option>.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setMaxBackupIndex myAppender 3</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setMaxBackupIndex()
{
  if [ $# -ne 2 ]; then
    _log4sh_error "appender_file_setMaxBackupIndex(): invalid number of parameters ($#)"
    return ${__LOG4SH_FALSE}
  fi

  afsmbi_appender=$1
  afsmbi_maxIndex=$2

  # TODO: put check for valid input

  afsmbi_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afsmbi_appender}`
  # TODO: put check for valid index here
  __log4shAppender_rollingFile_maxBackupIndexes=`_log4sh_setArrayElement \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" ${afsmbi_index} \
      "${afsmbi_maxIndex}"`
  __log4sh_return=$?

  # re-source the appender
  _appender_cache ${afsmbi_appender}

  unset afsmbi_appender afsmbi_maxIndex afsmbi_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>integer</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getMaxFileSize</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the maximum size that the output file is allowed to reach before
#     being rolled over to backup files.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>maxSize=`appender_file_getMaxBackupSize myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getMaxFileSize()
{
  if [ $# -ne 1 ]; then
    _log4sh_error "appender_file_getMaxFileSize(): invalid number of parameters ($#)"
    return ${__LOG4SH_FALSE}
  fi

  afgmfs_appender=$1

  afgmfs_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afgmfs_appender}`
  # TODO: put check for valid index here
  _log4sh_getArrayElement \
      "${__log4shAppender_rollingFile_maxFileSizes}" ${afgmfs_index}
  __log4sh_return=$?

  unset afgmfs_appender afgmfs_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setMaxFileSize</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>size</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the maximum size that the output file is allowed to reach before
#     being rolled over to backup files.
#   </para>
#   <para>
#     In configuration files, the <option>MaxFileSize</option> option takes an
#     long integer in the range 0 - 2^40. You can specify the value with the
#     suffixes "KiB", "MiB" or "GiB" so that the integer is interpreted being
#     expressed respectively in kilobytes, megabytes or gigabytes. For example,
#     the value "10KiB" will be interpreted as 10240.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setMaxBackupSize myAppender 10KiB</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setMaxFileSize()
{
  if [ $# -ne 2 ]; then
    _log4sh_error \
        "appender_file_setMaxFileSize(): invalid number of parameters ($#)"
    return ${__LOG4SH_ERROR}
  fi

  afsmfs_appender=$1
  afsmfs_size=$2
  afsmfs_return=${__LOG4SH_TRUE}

  # split the file size into parts
  afsmfs_value=`expr ${afsmfs_size} : '\([0-9]*\)'`
  afsmfs_unit=`expr ${afsmfs_size} : '[0-9]* *\([A-Za-z]\{1,3\}\)'`

  # determine multiplier
  if [ ${__log4sh_wa_strictBehavior} -eq ${__LOG4SH_TRUE} ]; then
    case "${afsmfs_unit}" in
      KB) afsmfs_unit='KiB' ;;
      MB) afsmfs_unit='MiB' ;;
      GB) afsmfs_unit='GiB' ;;
      TB) afsmfs_unit='TiB' ;;
    esac
  fi
  case "${afsmfs_unit}" in
    B) afsmfs_mul=1 ;;
    KB) afsmfs_mul=1000 ;;
    KiB) afsmfs_mul=1024 ;;
    MB) afsmfs_mul=1000000 ;;
    MiB) afsmfs_mul=1048576 ;;
    GB) afsmfs_mul=1000000000 ;;
    GiB) afsmfs_mul=1073741824 ;;
    TB) afsmfs_mul=1000000000000 ;;
    TiB) afsmfs_mul=1099511627776 ;;
    '')
      _log4sh_warn 'missing file size unit; assuming bytes'
      afsmfs_mul=1
      ;;
    *)
      _log4sh_error "unrecognized file size unit '${afsmfs_unit}'"
      afsmfs_return=${__LOG4SH_ERROR}
      ;;
  esac

  # calculate maximum file size
  if [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ]; then
    afsmfs_maxFileSize=`(expr ${afsmfs_value} \* ${afsmfs_mul} 2>&1)`
    if [ $? -gt 0 ]; then
      _log4sh_error "problem calculating maximum file size: '${afsmfs_maxFileSize}'"
      afsmfs_return=${__LOG4SH_FALSE}
    fi
  fi

  # store the maximum file size
  if [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ]; then
    afsmfs_index=`_log4sh_findArrayElement \
        "${__log4shAppenders}" ${afsmfs_appender}`
    # TODO: put check for valid index here
    __log4shAppender_rollingFile_maxFileSizes=`_log4sh_setArrayElement \
        "${__log4shAppender_rollingFile_maxFileSizes}" ${afsmfs_index} \
        "${afsmfs_maxFileSize}"`
  fi

  # re-source the appender
  [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ] \
      && _appender_cache ${afsmfs_appender}

  __log4sh_return=${afsmfs_return}
  unset afsmfs_appender afsmfs_size afsmfs_value afsmfs_unit afsmfs_mul \
      afsmfs_maxFileSize afsmfs_index afsmfs_return
  return ${__log4sh_return}
}

#-----------------------------------------------------------------------------
# SMTPAppender
#

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_getTo</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the to address for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>email=`appender_smtp_getTo myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_getTo()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_smtp_getTo(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgt_appender=$1

  asgt_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgt_appender}`
  # TODO: put check for valid index here
  asgt_to=`_log4sh_getArrayElement \
      "${__log4shAppender_smtp_tos}" ${asgt_index}`
  __log4sh_return=$?

  [ "${asgt_to}" = "${__LOG4SH_NULL}" ] && asgt_to=''
  echo "${asgt_to}"

  unset asgt_appender asgt_index asgt_to
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_setTo</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>email</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the to address for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setTo myAppender user@example.com</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_setTo()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_smtp_setTo(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asst_appender=$1
  asst_email=$2

  asst_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asst_appender}`
  # TODO: put check for valid index here
  __log4shAppender_smtp_tos=`_log4sh_setArrayElement \
    "${__log4shAppender_smtp_tos}" ${asst_index} "${asst_email}"`

  # resource the appender
  _appender_cache ${asst_appender}

  unset asst_appender asst_email asst_index
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderRecipient</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>email</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Set the to address for the given appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setTo myAppender user@example.com</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderRecipient()
{
  appender_smtp_setTo "$@"
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_getSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the email subject for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>subject=`appender_smtp_getSubject myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_getSubject()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_smtp_getSubject(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgs_appender=$1

  asgs_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgs_appender}`
  # TODO: put check for valid index here
  asgs_subject=`_log4sh_getArrayElement \
      "${__log4shAppender_smtp_subjects}" ${asgs_index}`
  __log4sh_return=$?

  [ "${asgs_subject}" = "${__LOG4SH_NULL}" ] && asgs_subject=''
  echo "${asgs_subject}"

  unset asgs_appender asgs_index asgs_subject
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_setSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>subject</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the email subject for an SMTP appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setSubject myAppender "This is a test"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_setSubject()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_smtp_setSubject(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asss_appender=$1
  asss_subject=$2

  # set the Subject
  asss_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asss_appender}`
  if [ ${asss_index} -gt 0 ]; then
    __log4shAppender_smtp_subjects=`_log4sh_setArrayElement \
      "${__log4shAppender_smtp_subjects}" ${asss_index} "${asss_subject}"`
    __log4sh_return=${__LOG4SH_TRUE}
  else
    _log4sh_error "could not set Subject for appender (${asss_appender})"
    __log4sh_return=${__LOG4SH_FALSE}
  fi

  # re-source the appender
  _appender_cache ${asss_appender}

  unset asss_appender asss_subject asss_index
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>subject</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Sets the email subject for an SMTP appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderSubject myAppender "This is a test"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderSubject()
{
  appender_smtp_setSubject "$@"
}

#-----------------------------------------------------------------------------
# SyslogAppender
#

#/**
# <s:function group="SyslogAppender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef>
#         <function>_appender_syslog_getFacilityByIndex</function>
#       </funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the syslog facility of the specified appender by index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>
#       facility=`_appender_syslog_getFacilityByIndex 3`
#     </funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_syslog_getFacilityByIndex()
{
  _log4sh_getArrayElement "$__log4shAppender_syslog_facilities" $1
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getSyslogFacility</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Get the syslog facility of the specified appender by index
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>facility=`appender_getSyslogFacility 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getSyslogFacility()
{
  _appender_syslog_getFacilityByIndex "$@"
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_getFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the syslog facility for the given appender.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>facility=`appender_syslog_getFacility myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_getFacility()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_syslog_getFacility(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgf_appender=$1

  asgf_index=`_log4sh_findArrayElement "$__log4shAppenders" ${asgf_appender}`
  _log4sh_getArrayElement "${__log4shAppender_syslog_facilities}" ${asgf_index}

  unset asgf_appender asgf_index
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_setFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>facility</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the syslog facility for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_syslog_setFacility myAppender local4`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_setFacility()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_syslog_setFacility(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi
  assf_appender=$1
  assf_facility=$2

  # check for valid facility
  echo "${__LOG4SH_TYPE_SYSLOG_FACILITY_NAMES}" |grep " ${assf_facility} " >/dev/null
  if [ $? -ne 0 ]; then
    # the facility is not valid
    _log4sh_error "[${assf_facility}] is an unknown syslog facility. Defaulting to [user]."
    assf_facility='user'
  fi

  # set appender facility
  assf_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${assf_appender}`
  # TODO: put check for valid index here
  __log4shAppender_syslog_facilities=`_log4sh_setArrayElement \
    "${__log4shAppender_syslog_facilities}" ${assf_index} "${assf_facility}"`

  # re-source the appender
  _appender_cache ${assf_appender}

  unset assf_appender assf_facility assf_index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setSyslogFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>facility</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.2</emphasis></para>
#   <para>
#     Set the syslog facility for the given appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setSyslogFacility myAppender local4`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setSyslogFacility()
{
  appender_syslog_setFacility "$@"
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_getHost</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the syslog host of the specified appender.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>host=`appender_syslog_getHost myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_getHost()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_syslog_getHost(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgh_appender=$1

  asgh_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgh_appender}`
  # TODO: put check for valid index here
  asgh_host=`_log4sh_getArrayElement \
      "${__log4shAppender_syslog_hosts}" ${asgh_index}`
  __log4sh_return=$?

  [ "${asgh_host}" = "${__LOG4SH_NULL}" ] && asgh_host=''
  echo "${asgh_host}"

  unset asgh_appender asgh_index asgh_host
  return ${__log4sh_return}
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_setHost</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>host</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the syslog host for the given appender. Requires that the 'nc'
#     command alternative has been previously set with the
#     log4sh_setAlternative() function.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_syslog_setHost myAppender localhost</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
#
# The BSD syslog Protocol
#   http://www.ietf.org/rfc/rfc3164.txt
#
appender_syslog_setHost()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_syslog_setHost(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  assh_appender=$1
  assh_host=$2

  [ -z "${__log4sh_alternative_nc:-}" ] \
      && _log4sh_warn 'the nc (netcat) command alternative is required for remote syslog logging. see log4sh_setAlternative().'

  assh_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${assh_appender}`
  # TODO: put check for valid index here
  __log4shAppender_syslog_hosts=`_log4sh_setArrayElement \
      "${__log4shAppender_syslog_hosts}" ${assh_index} "${assh_host}"`

  # re-source the appender
  _appender_cache ${assh_appender}

  unset assh_appender assh_host assh_index
  return ${__LOG4SH_TRUE}
}

#=============================================================================
# Level
#

#/**
# <s:function group="Level" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_level_toLevel</function></funcdef>
#       <paramdef>integer <parameter>val</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Converts an internally used level integer into its external level
#   equivalent</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>level=`logger_level_toLevel 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
# TODO use arrays instead of case statement ??
logger_level_toLevel()
{
  _ltl__val=$1

  _ltl__return=${__LOG4SH_TRUE}
  _ltl__level=''

  case ${_ltl__val} in
    ${__LOG4SH_LEVEL_TRACE}) _ltl__level=${__LOG4SH_LEVEL_TRACE_STR} ;;
    ${__LOG4SH_LEVEL_DEBUG}) _ltl__level=${__LOG4SH_LEVEL_DEBUG_STR} ;;
    ${__LOG4SH_LEVEL_INFO}) _ltl__level=${__LOG4SH_LEVEL_INFO_STR} ;;
    ${__LOG4SH_LEVEL_WARN}) _ltl__level=${__LOG4SH_LEVEL_WARN_STR} ;;
    ${__LOG4SH_LEVEL_ERROR}) _ltl__level=${__LOG4SH_LEVEL_ERROR_STR} ;;
    ${__LOG4SH_LEVEL_FATAL}) _ltl__level=${__LOG4SH_LEVEL_FATAL_STR} ;;
    ${__LOG4SH_LEVEL_OFF}) _ltl__level=${__LOG4SH_LEVEL_OFF_STR} ;;
    ${__LOG4SH_LEVEL_CLOSED}) _ltl__level=${__LOG4SH_LEVEL_CLOSED_STR} ;;
    *) _ltl__return=${__LOG4SH_FALSE} ;;
  esac

  echo ${_ltl__level}
  unset _ltl__val _ltl__level
  return ${_ltl__return}
}

#/**
# <s:function group="Level" modifier="public">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_level_toInt</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Converts an externally used level tag into its integer
#   equivalent</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>levelInt=`logger_level_toInt WARN`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_level_toInt()
{
  _lti__level=$1

  _lti__int=0
  _lti__return=${__LOG4SH_TRUE}

  case ${_lti__level} in
    ${__LOG4SH_LEVEL_TRACE_STR}) _lti__int=${__LOG4SH_LEVEL_TRACE} ;;
    ${__LOG4SH_LEVEL_DEBUG_STR}) _lti__int=${__LOG4SH_LEVEL_DEBUG} ;;
    ${__LOG4SH_LEVEL_INFO_STR}) _lti__int=${__LOG4SH_LEVEL_INFO} ;;
    ${__LOG4SH_LEVEL_WARN_STR}) _lti__int=${__LOG4SH_LEVEL_WARN} ;;
    ${__LOG4SH_LEVEL_ERROR_STR}) _lti__int=${__LOG4SH_LEVEL_ERROR} ;;
    ${__LOG4SH_LEVEL_FATAL_STR}) _lti__int=${__LOG4SH_LEVEL_FATAL} ;;
    ${__LOG4SH_LEVEL_OFF_STR}) _lti__int=${__LOG4SH_LEVEL_OFF} ;;
    ${__LOG4SH_LEVEL_CLOSED_STR}) _lti__int=${__LOG4SH_LEVEL_CLOSED} ;;
    *) _lti__return=${__LOG4SH_FALSE} ;;
  esac

  echo ${_lti__int}
  unset _lti__int _lti__level
  return ${_lti__return}
}

#=============================================================================
# Logger
#

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_addAppender</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Add and initialize a new appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_addAppender $appender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_addAppender()
{
  laa_appender=$1

  # FAQ should we be using setter functions here?? for performance, no.
  __log4shAppenders=`_log4sh_pushStack "${__log4shAppenders}" ${laa_appender}`
  __log4shAppenderCount=`expr ${__log4shAppenderCount} + 1`
  __log4shAppenderCounts="${__log4shAppenderCounts} ${__log4shAppenderCount}"
  __log4shAppenderLayouts=`_log4sh_pushStack \
      "$__log4shAppenderLayouts" "${__LOG4SH_LAYOUT_SIMPLE}"`
  __log4shAppenderLevels=`_log4sh_pushStack \
      "${__log4shAppenderLevels}" "${__LOG4SH_NULL}"`
  __log4shAppenderPatterns=`_log4sh_pushStack \
      "${__log4shAppenderPatterns}" "${__LOG4SH_PATTERN_DEFAULT}"`
  __log4shAppenderTypes=`_log4sh_pushStack \
      "${__log4shAppenderTypes}" ${__LOG4SH_TYPE_CONSOLE}`
  __log4shAppender_file_files=`_log4sh_pushStack \
      "${__log4shAppender_file_files}" ${__LOG4SH_NULL}`
  __log4shAppender_rollingFile_maxBackupIndexes=`_log4sh_pushStack \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" \
      ${__LOG4SH_TYPE_ROLLING_FILE_MAX_BACKUP_INDEX}`
  __log4shAppender_rollingFile_maxFileSizes=`_log4sh_pushStack \
      "${__log4shAppender_rollingFile_maxFileSizes}" \
      ${__LOG4SH_TYPE_ROLLING_FILE_MAX_FILE_SIZE}`
  __log4shAppender_smtp_tos=`_log4sh_pushStack \
      "${__log4shAppender_smtp_tos}" ${__LOG4SH_NULL}`
  __log4shAppender_smtp_subjects=`_log4sh_pushStack \
      "${__log4shAppender_smtp_subjects}" ${__LOG4SH_NULL}`
  __log4shAppender_syslog_facilities=`_log4sh_pushStack \
      "${__log4shAppender_syslog_facilities}" ${__LOG4SH_TYPE_SYSLOG_FACILITY}`
  __log4shAppender_syslog_hosts=`_log4sh_pushStack \
      "${__log4shAppender_syslog_hosts}" "${__LOG4SH_NULL}"`

  _appender_cache ${laa_appender}

  unset laa_appender
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_addAppenderWithPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.6</emphasis></para>
#   <para>
#     Add and initialize a new appender with a specific PatternLayout
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_addAppenderWithPattern $appender '%d %p - %m%n'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_addAppenderWithPattern()
{
  _myAppender=$1
  _myPattern=$2

  logger_addAppender ${_myAppender}
  appender_setLayout ${_myAppender} ${__LOG4SH_LAYOUT_PATTERN}
  appender_setPattern ${_myAppender} "${_myPattern}"

  unset _myAppender _myPattern
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getFilename</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the filename that would be shown when the '%F' conversion character
#     is used in a PatternLayout.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>filename=`logger_getFilename`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getFilename()
{
  echo "${__log4sh_filename}"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setFilename</function></funcdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the filename to be shown when the '%F' conversion character is
#   used in a PatternLayout.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setFilename 'myScript.sh'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setFilename()
{
  __log4sh_filename=$1
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getLevel</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the global default logging level (e.g. DEBUG).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>level=`logger_getLevel`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getLevel()
{
  logger_level_toLevel ${__log4shLevel}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setLevel</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the global default logging level (e.g. DEBUG).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setLevel INFO</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setLevel()
{
  _l_level=$1

  _l_int=`logger_level_toInt ${_l_level}`
  if [ $? -eq ${__LOG4SH_TRUE} ]; then
    __log4shLevel=${_l_int}
  else
    _log4sh_error "attempt to set invalid log level '${_l_level}'"
  fi

  unset _l_int _l_level
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#       <paramdef>string[] <parameter>message(s)</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>The base logging command that logs a message to all defined
#     appenders</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log DEBUG 'This is a test message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log()
{
  _l_level=$1
  shift
  # if no message was passed, read it from STDIN
  [ $# -ne 0 ] && _l_msg="$@" || _l_msg=`cat`

  __log4sh_return=${__LOG4SH_TRUE}
  _l_levelInt=`logger_level_toInt ${_l_level}`
  if [ $? -eq ${__LOG4SH_TRUE} ]; then
    # update seconds elapsed
    _log4sh_updateSeconds

    _l_oldIFS=${IFS} IFS=${__LOG4SH_IFS_DEFAULT}
    for _l_appenderIndex in ${__log4shAppenderCounts}; do
      ${__LOG4SH_TRACE} "_l_appenderIndex='${_l_appenderIndex}'"
      # determine appender level
      _l_appenderLevel=`_appender_getLevelByIndex ${_l_appenderIndex}`
      if [ "${_l_appenderLevel}" = "${__LOG4SH_NULL}" ]; then
        # continue if requested is level less than general level
        [ ! ${__log4shLevel} -le ${_l_levelInt} ] && continue
      else
        _l_appenderLevelInt=`logger_level_toInt ${_l_appenderLevel}`
        # continue if requested level is less than specific appender level
        ${__LOG4SH_TRACE} "_l_levelInt='${_l_levelInt}' _l_appenderLevelInt='${_l_appenderLevelInt}'"
        [ ! ${_l_appenderLevelInt} -le ${_l_levelInt} ] && continue
      fi

      # execute dynamic appender function
      _l_appenderName=`_log4sh_getArrayElement \
        "${__log4shAppenders}" ${_l_appenderIndex}`
      ${__LOG4SH_APPENDER_FUNC_PREFIX}${_l_appenderName}_append ${_l_level} "${_l_msg}"
    done
    IFS=${_l_oldIFS}
  else
    _log4sh_error "invalid logging level requested (${_l_level})"
    __log4sh_return=${__LOG4SH_ERROR}
  fi

  unset _l_msg _l_oldIFS _l_level _l_levelInt
  unset _l_appenderIndex _l_appenderLevel _l_appenderLevelInt _l_appenderName
  return ${__log4sh_return}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_trace</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the TRACE
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_trace 'This is a trace message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_trace()
{
  log ${__LOG4SH_LEVEL_TRACE_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_debug</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the DEBUG
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_debug 'This is a debug message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_debug()
{
  log ${__LOG4SH_LEVEL_DEBUG_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_info</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the INFO
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_info 'This is a info message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_info()
{
  log ${__LOG4SH_LEVEL_INFO_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_warn</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is a helper function for logging a message at the WARN priority
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_warn 'This is a warn message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_warn()
{
  log ${__LOG4SH_LEVEL_WARN_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_error</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is a helper function for logging a message at the ERROR priority
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_error 'This is a error message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_error()
{
  log ${__LOG4SH_LEVEL_ERROR_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_fatal</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the FATAL
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_fatal 'This is a fatal message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_fatal()
{
  log ${__LOG4SH_LEVEL_FATAL_STR} "$@"
}

#==============================================================================
# Property
#

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getPropPrefix</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Takes a string (eg. "log4sh.appender.stderr.File") and returns the
#   prefix of it (everything before the first '.' char). Normally used in
#   parsing the log4sh configuration file.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>prefix=`_log4sh_getPropPrefix $property"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getPropPrefix()
{
  _oldIFS=${IFS} IFS='.'
  set -- $1
  IFS=${_oldIFS} unset _oldIFS
  echo $1
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_stripPropPrefix</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Strips the prefix off a property configuration command and returns
#   the string. E.g. "log4sh.appender.stderr.File" becomes
#   "appender.stderr.File".</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newProperty=`_log4sh_stripPropPrefix $property`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_stripPropPrefix()
{
  expr "$1" : '[^.]*\.\(.*\)'
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propAlternative</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Configures log4sh to use an alternative command.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propAlternative property value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propAlternative()
{
  _lpa_key=$1
  _lpa_value=$2

  # strip the leading 'alternative.'
  _lpa_alternative=`_log4sh_stripPropPrefix ${_lpa_key}`

  # set the alternative
  log4sh_setAlternative ${_lpa_alternative} "${_lpa_value}"

  unset _lpa_key _lpa_value _lpa_alternative
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propAppender</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Configures log4sh using an appender property configuration statement</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propAppender $property $value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propAppender()
{
  _lpa_key=$1
  _lpa_value=$2

  _lpa_appender=''
  _lpa_rtrn=${__LOG4SH_TRUE}

  # strip the leading 'appender' keyword prefix
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`

  # handle appender definitions
  if [ "${_lpa_key}" '=' "`expr \"${_lpa_key}\" : '\([^.]*\)'`" ]; then
    _lpa_appender="${_lpa_key}"
  else
    _lpa_appender=`_log4sh_getPropPrefix ${_lpa_key}`
  fi

  # does the appender exist?
  appender_exists ${_lpa_appender}
  if [ $? -eq ${__LOG4SH_FALSE} ]; then
    _log4sh_error "attempt to configure the non-existant appender (${_lpa_appender})"
    unset _lpa_appender _lpa_key _lpa_value
    return ${__LOG4SH_ERROR}
  fi

  # handle the appender type
  if [ "${_lpa_appender}" = "${_lpa_key}" ]; then
    case ${_lpa_value} in
      ${__LOG4SH_TYPE_CONSOLE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_CONSOLE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_CONSOLE} ;;
      ${__LOG4SH_TYPE_FILE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_FILE} ;;
      $__LOG4SH_TYPE_DAILY_ROLLING_FILE|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_DAILY_ROLLING_FILE} ;;
      ${__LOG4SH_TYPE_ROLLING_FILE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_ROLLING_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_ROLLING_FILE} ;;
      ${__LOG4SH_TYPE_SMTP}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_SMTP})
        appender_setType $_lpa_appender ${__LOG4SH_TYPE_SMTP} ;;
      ${__LOG4SH_TYPE_SYSLOG}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_SYSLOG})
        appender_setType $_lpa_appender ${__LOG4SH_TYPE_SYSLOG} ;;
      *)
        _log4sh_error "appender type (${_lpa_value}) unrecognized"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
    __log4sh_return=${_lpa_rtrn}
    unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
    return ${__log4sh_return}
  fi

  # handle appender values and methods
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`
  if [ "${_lpa_key}" '=' "`expr \"${_lpa_key}\" : '\([^.]*\)'`" ]; then
    case ${_lpa_key} in
      # General
      Threshold) appender_setLevel ${_lpa_appender} "${_lpa_value}" ;;
      layout) appender_setLayout ${_lpa_appender} "${_lpa_value}" ;;

      # FileAppender
      DatePattern) ;;  # unsupported
      File)
        _lpa_value=`eval echo "${_lpa_value}"`
        appender_file_setFile ${_lpa_appender} "${_lpa_value}"
        ;;
      MaxBackupIndex)
        appender_file_setMaxBackupIndex ${_lpa_appender} "${_lpa_value}" ;;
      MaxFileSize)
        appender_file_setMaxFileSize ${_lpa_appender} "${_lpa_value}" ;;

      # SMTPAppender
      To) appender_smtp_setTo ${_lpa_appender} "${_lpa_value}" ;;
      Subject) appender_smtp_setSubject ${_lpa_appender} "${_lpa_value}" ;;

      # SyslogAppender
      SyslogHost) appender_syslog_setHost ${_lpa_appender} "${_lpa_value}" ;;
      Facility) appender_syslog_setFacility ${_lpa_appender} "${_lpa_value}" ;;

      # catch unrecognized
      *)
        _log4sh_error "appender value/method (${_lpa_key}) unrecognized"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
    __log4sh_return=${_lpa_rtrn}
    unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
    return ${__log4sh_return}
  fi

  # handle appender layout values and methods
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`
  case ${_lpa_key} in
    ConversionPattern) appender_setPattern ${_lpa_appender} "${_lpa_value}" ;;
    *)
      _log4sh_error "layout value/method (${_lpa_key}) unrecognized"
      false
      ;;
  esac
  [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
  __log4sh_return=${_lpa_rtrn}
  unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
  return ${__log4sh_return}
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propLogger</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>(future) Configures log4sh with a <code>logger</code> configuration
#   statement. Sample output: "logger: property value".</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>result=`_log4sh_propLogger $property $value`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propLogger()
{
  _prop=`_log4sh_stripPropPrefix $1`
  echo "logger: ${_prop} $2"
  unset _prop
}

#
# configure log4sh with a rootLogger configuration statement
#
# @param  _key    configuration command
# @param  _value  configuration value
#
#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propRootLogger</function></funcdef>
#       <paramdef>string <parameter>rootLogger</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Configures log4sh with a <code>rootLogger</code> configuration
#   statement. It expects a comma separated string similar to the following:</para>
#   <para><code>log4sh.rootLogger=ERROR, stderr, R</code></para>
#   <para>The first option is the default logging level to set for all
#   of the following appenders that will be created, and all following options
#   are the names of appenders to create. The appender names must be
#   unique.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propRootLogger $value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propRootLogger()
{
  __lprl_rootLogger=`echo "$@" |sed 's/ *, */,/g'`
  __lprl_count=`echo "${__lprl_rootLogger}" |sed 's/,/ /g' |wc -w`
  __lprl_index=1
  while [ ${__lprl_index} -le ${__lprl_count} ]; do
    __lprl_operand=`echo "${__lprl_rootLogger}" |cut -d, -f${__lprl_index}`
    if [ ${__lprl_index} -eq 1 ]; then
      logger_setLevel "${__lprl_operand}"
    else
      appender_exists "${__lprl_operand}"
      if [ $? -eq ${__LOG4SH_FALSE} ]; then
        logger_addAppender "${__lprl_operand}"
      else
        _log4sh_error "attempt to add already existing appender of name (${__lprl_operand})"
      fi
    fi
    __lprl_index=`expr ${__lprl_index} + 1`
  done

  unset __lprl_count __lprl_index __lprl_operand __lprl_rootLogger
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_doConfigure</function></funcdef>
#       <paramdef>string <parameter>configFileName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Read configuration from a file. <emphasis role="strong">The existing
#     configuration is not cleared or reset.</emphasis> If you require a
#     different behavior, then call the <code>log4sh_resetConfiguration</code>
#     before calling <code>log4sh_doConfigure</code>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_doConfigure myconfig.properties</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_doConfigure()
{
  [ -n "${FUNCNAME:-}" ] \
      && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"

  # prepare the environment for configuration
  log4sh_resetConfiguration

  ldc_file=$1
  ldc_rtrn=${__LOG4SH_TRUE}

  # strip the config prefix and dump output to a temporary file
  ldc_tmpFile="${__log4sh_tmpDir}/properties"
  ${__LOG4SH_TRACE} "__LOG4SH_CONFIG_PREFIX='${__LOG4SH_CONFIG_PREFIX}'"
  grep "^${__LOG4SH_CONFIG_PREFIX}\." "${ldc_file}" >"${ldc_tmpFile}"

  # read the file in. using a temporary file and a file descriptor here instead
  # of piping the file into the 'while read' because the pipe causes a fork
  # under some shells which makes it impossible to get the variables passed
  # back to the parent script.
  exec 3<&0 <"${ldc_tmpFile}"
  while read ldc_line; do
    ldc_key=`expr "${ldc_line}" : '\([^= ]*\) *=.*'`
    ldc_value=`expr "${ldc_line}" : '[^= ]* *= *\(.*\)'`

    # strip the leading 'log4sh.'
    ldc_key=`_log4sh_stripPropPrefix ${ldc_key}`
    ldc_keyword=`_log4sh_getPropPrefix ${ldc_key}`
    case ${ldc_keyword} in
      alternative) _log4sh_propAlternative ${ldc_key} "${ldc_value}" ;;
      appender) _log4sh_propAppender ${ldc_key} "${ldc_value}" ;;
      logger) _log4sh_propLogger ${ldc_key} "${ldc_value}" ;;
      rootLogger) _log4sh_propRootLogger "${ldc_value}" ;;
      *)
        _log4sh_error "unrecognized properties keyword (${ldc_keyword})"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && ldc_rtrn=${__LOG4SH_ERROR}
  done
  exec 0<&3 3<&-

  # remove the temporary file
  rm -f "${ldc_tmpFile}"

  # activate all of the appenders
  for ldc_appender in ${__log4shAppenders}; do
    ${__LOG4SH_APPENDER_FUNC_PREFIX}${ldc_appender}_activateOptions
  done

  __log4sh_return=${ldc_rtrn}
  unset ldc_appender ldc_file ldc_tmpFile ldc_line ldc_key ldc_keyword
  unset ldc_value ldc_rtrn
  return ${__log4sh_return}
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_readProperties</function></funcdef>
#       <paramdef>string <parameter>configFileName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.6</emphasis></para>
#   <para>
#     See <code>log4sh_doConfigure</code>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_readProperties myconfig.properties</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_readProperties()
{
  log4sh_doConfigure "$@"
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_resetConfiguration</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This function completely resets the log4sh configuration to have no
#     appenders with a global logging level of ERROR.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_resetConfiguration</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
# XXX if a configuration is *repeatedly* established via logger_addAppender and
# reset using this command, there is a risk of running out of memory.
log4sh_resetConfiguration()
{
  __log4shAppenders=''
  __log4shAppenderCount=0
  __log4shAppenderCounts=''
  __log4shAppenderLayouts=''
  __log4shAppenderLevels=''
  __log4shAppenderPatterns=''
  __log4shAppenderTypes=''
  __log4shAppender_file_files=''
  __log4shAppender_rollingFile_maxBackupIndexes=''
  __log4shAppender_rollingFile_maxFileSizes=''
  __log4shAppender_smtp_tos=''
  __log4shAppender_smtp_subjects=''
  __log4shAppender_syslog_facilities=''
  __log4shAppender_syslog_hosts=''

  logger_setLevel ERROR
}

#==============================================================================
# Thread
#

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getThreadName</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current thread name.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>threadName=`logger_getThreadName`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getThreadName()
{
  echo ${__log4sh_threadName}
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setThreadName</function></funcdef>
#       <paramdef>string <parameter>threadName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Sets the thread name (e.g. the name of the script). This thread name can
#     be used with the '%t' conversion character within a
#     <option>PatternLayout</option>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setThreadName "myThread"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setThreadName()
{
  _thread=$1

  _length=`_log4sh_getArrayLength "$__log4sh_threadStack"`
  __log4sh_threadStack=`_log4sh_setArrayElement "$__log4sh_threadStack" $_length $_thread`
  __log4sh_threadName=$_thread

  unset _length _thread
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_pushThreadName</function></funcdef>
#       <paramdef>string <parameter>threadName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.7</emphasis></para>
#   <para>
#     Sets the thread name (eg. the name of the script) and pushes the old on
#     to a stack for later use. This thread name can be used with the '%t'
#     conversion character within a <option>PatternLayout</option>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_pushThreadName "myThread"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_pushThreadName()
{
  __log4sh_threadStack=`_log4sh_pushStack "$__log4sh_threadStack" $1`
  __log4sh_threadName=$1
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_popThreadName</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.7</emphasis></para>
#   <para>
#     Removes the topmost thread name from the stack. The next thread name on
#     the stack is then placed in the <varname>__log4sh_threadName</varname>
#     variable. If the stack is empty, or has only one element left, then a
#     warning is given that no more thread names can be popped from the stack.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_popThreadName</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_popThreadName()
{
  _length=`_log4sh_getArrayLength "$__log4sh_threadStack"`
  if [ $_length -gt 1 ]; then
    __log4sh_threadStack=`_log4sh_popStack "$__log4sh_threadStack"`
    __log4sh_threadName=`_log4sh_peekStack "$__log4sh_threadStack"`
  else
    echo 'log4sh:WARN no more thread names available on thread name stack.' >&2
  fi
}

#==============================================================================
# Trap
#

#/**
# <s:function group="Trap" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_cleanup</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a cleanup function to remove the temporary directory used by
#   log4sh. It is provided for scripts who want to do log4sh cleanup work
#   themselves rather than using the automated cleanup of log4sh that is
#   invoked upon a normal exit of the script.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_cleanup</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_cleanup()
{
  _log4sh_cleanup 'EXIT'
}

#/**
# <s:function group="Trap" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_cleanup</function></funcdef>
#       <paramdef>string <parameter>signal</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a cleanup function to remove the temporary directory used by
#   log4sh. It should only be called by log4sh itself when it is taking
#   control of traps.</para>
#   <para>If there was a previously defined trap for the given signal, log4sh
#   will attempt to call the original trap handler as well so as not to break
#   the parent script.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_cleanup EXIT</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_cleanup()
{
  _lc__trap=$1
  ${__LOG4SH_INFO} "_log4sh_cleanup(): the ${_lc__trap} signal was caught"

  _lc__restoreTrap=${__LOG4SH_FALSE}
  _lc__oldTrap=''

  # match trap to signal value
  case "${_lc__trap}" in
    EXIT) _lc__signal=0 ;;
    INT) _lc__signal=2 ;;
    TERM) _lc__signal=15 ;;
  esac

  # do we possibly need to restore a previous trap?
  if [ -r "${__log4sh_trapsFile}" -a -s "${__log4sh_trapsFile}" ]; then
    # yes. figure out what we need to do
    if [ `grep "^trap -- " "${__log4sh_trapsFile}" >/dev/null; echo $?` -eq 0 ]
    then
      # newer trap command
      ${__LOG4SH_DEBUG} 'newer POSIX trap command'
      _lc__restoreTrap=${__LOG4SH_TRUE}
      _lc__oldTrap=`egrep "(${_lc__trap}|${_lc__signal})$" "${__log4sh_trapsFile}" |\
        sed "s/^trap -- '\(.*\)' [A-Z]*$/\1/"`
    elif [ `grep "[0-9]*: " "${__log4sh_trapsFile}" >/dev/null; echo $?` -eq 0 ]
    then
      # older trap command
      ${__LOG4SH_DEBUG} 'older style trap command'
      _lc__restoreTrap=${__LOG4SH_TRUE}
      _lc__oldTrap=`grep "^${_lc__signal}: " "${__log4sh_trapsFile}" |\
        sed 's/^[0-9]*: //'`
    else
      # unrecognized trap output
      _log4sh_error 'unable to restore old traps! unrecognized trap command output'
    fi
  fi

  # do our work
  rm -fr "${__log4sh_tmpDir}"

  # execute the old trap
  if [ ${_lc__restoreTrap} -eq ${__LOG4SH_TRUE} -a -n "${_lc__oldTrap}" ]; then
    ${__LOG4SH_INFO} 'restoring previous trap of same type'
    eval "${_lc__oldTrap}"
  fi

  # exit for all non-EXIT signals
  if [ "${_lc__trap}" != 'EXIT' ]; then
    # disable the EXIT trap
    trap 0

    # add 127 to signal value and exit
    _lc__signal=`expr ${_lc__signal} + 127`
    exit ${_lc__signal}
  fi

  unset _lc__oldTrap _lc__signal _lc__restoreTrap _lc__trap
  return
}


#==============================================================================
# main
#

# create a temporary directory
__log4sh_tmpDir=`_log4sh_mktempDir`

# preserve old trap(s)
__log4sh_trapsFile="${__log4sh_tmpDir}/traps"
trap >"${__log4sh_trapsFile}"

# configure traps
${__LOG4SH_INFO} 'setting traps'
trap '_log4sh_cleanup EXIT' 0
trap '_log4sh_cleanup INT' 2
trap '_log4sh_cleanup TERM' 15

# alternative commands
log4sh_setAlternative mail "${LOG4SH_ALTERNATIVE_MAIL:-mail}" ${__LOG4SH_TRUE}
[ -n "${LOG4SH_ALTERNATIVE_NC:-}" ] \
    && log4sh_setAlternative nc "${LOG4SH_ALTERNATIVE_NC}"

# load the properties file
${__LOG4SH_TRACE} "__LOG4SH_CONFIGURATION='${__LOG4SH_CONFIGURATION}'"
if [ "${__LOG4SH_CONFIGURATION}" != 'none' -a -r "${__LOG4SH_CONFIGURATION}" ]
then
  ${__LOG4SH_INFO} 'configuring via properties file'
  log4sh_doConfigure "${__LOG4SH_CONFIGURATION}"
else
  if [ "${__LOG4SH_CONFIGURATION}" != 'none' ]; then
    _log4sh_warn 'No appenders could be found.'
    _log4sh_warn 'Please initalize the log4sh system properly.'
  fi
  ${__LOG4SH_INFO} 'configuring at runtime'

  # prepare the environment for configuration
  log4sh_resetConfiguration

  # note: not using the constant variables here (e.g. for ConsoleAppender) so
  # that those perusing the code can have a working example
  logger_setLevel ${__LOG4SH_LEVEL_ERROR_STR}
  logger_addAppender stdout
  appender_setType stdout ConsoleAppender
  appender_setLayout stdout PatternLayout
  appender_setPattern stdout '%-4r [%t] %-5p %c %x - %m%n'
fi

# restore the previous set of shell flags
for _log4sh_shellFlag in ${__LOG4SH_SHELL_FLAGS}; do
  echo ${__log4sh_oldShellFlags} |grep ${_log4sh_shellFlag} >/dev/null \
    || set +${_log4sh_shellFlag}
done
unset _log4sh_shellFlag

#/**
# </s:shelldoc>
#*/
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/lib/migrate�����������������������������������������������0000644�0000765�0000024�00000002771�11233004154�022270� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/lib/log4sh

function mark_complete {
    migration=$1

    date >> deploy/state/$migration
    logger_info "Migration $migration ran successfully."
}

function run_migration {
    migration=$1

    logger_info "Running $migration migration."

    if sh deploy/migrations/$migration
    then
        mark_complete $migration
    else
        logger_error "Script $migration failed, exiting."
        exit 1
    fi
}

function migrate {
    TARGET=$1

    logger_info "Changing to $TARGET"

    pushd $TARGET
    assert_in $TARGET

    for migration in $(ls deploy/migrations | sort -n)
    do
        if [ -e deploy/state/$migration ]
        then
            logger_info "Skipping $migration migration."
        else
            run_migration $migration
        fi
    done

    popd
}


function backup {

    logger_info "Backing up $1 to $2 before deployment to "
    
    rdiff-backup $1 $2
    rdiff-backup -l $2
}

function rollback {
    BACK_BY=$1
    SOURCE=$2
    TARGET=$3

    logger_info "Rolling back to $BACK_BY backups ago."

    rdiff-backup -r $BACK_BY $SOURCE $TARGET
}

function deploy_code {
    logger_info "Deploying code from $1 to $2."

    rsync --exclude config -av $1 $2
}

function assert_in {
    if [ "$PWD" != "$1" ]
    then
        logger_error "You must be in the source checkout directory!"
        exit 1
    fi
}

function assert_env {
    if [ "$SOURCE" == "" ]
    then
        logger_error "You must specify an environment (testing/prod/staging)".
        exit 1
    fi
}


�������lamson-1.0pre11/examples/librelist/deploy/lib/shunit2�����������������������������������������������0000644�0000765�0000024�00000054500�11232747060�022242� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# $Id: shunit2 71 2007-06-02 18:42:46Z sfsetse $
# vim:syntax=sh:sts=2
# vim:foldmethod=marker:foldmarker=/**,*/
#
#/**
# <?xml version="1.0" encoding="UTF-8"?>
# <s:shelldoc xmlns:s="http://www.forestent.com/projects/shelldoc/xsl/2005.0">
# <s:header>
# shUnit 2.1.0
# Shell Unit Test Framework
#
# http://shunit2.sourceforge.net/
#
# written by Kate Ward &lt;kate.ward@forestent.com&gt;
# released under the LGPL
#
# this module implements a xUnit based unit test framework similar to JUnit
# </s:header>
#*/

# shell flags for shunit:
# u - treat unset variables as an error when performing parameter expansion
__SHUNIT_SHELL_FLAGS='u'

# save the current set of shell flags, and then set some for ourselves
__shunit_oldShellFlags="$-"
for _shunit_shellFlag in `echo "${__SHUNIT_SHELL_FLAGS}" |sed 's/\(.\)/\1 /g'`
do
  set -${_shunit_shellFlag}
done

# constants

__SHUNIT_VERSION='2.1.1pre'

SHUNIT_TRUE=0
SHUNIT_FALSE=1

__SHUNIT_ASSERT_MSG_PREFIX='ASSERT:'

for _su_const in `set |grep "^__SHUNIT_" |cut -d= -f1`; do
  readonly ${_su_const}
done
unset _su_const

# variables
__shunit_skip=${SHUNIT_FALSE}
__shunit_suite=''

__shunit_testsPassed=0
__shunit_testsFailed=0
__shunit_testsSkipped=0
__shunit_testsTotal=0

#-----------------------------------------------------------------------------
# assert functions
#

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertEquals</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
assertEquals()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 3 ]; then
    _su_message=$1
    shift
  fi
  _su_expected=$1
  _su_actual=$2

  if [ "${_su_expected}" = "${_su_actual}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_expected _su_actual
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNull</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>value</emphasis> is <literal>null</literal>,
#   or in shell terms a zero-length string. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNull()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_value=$1

  _su_value2=`eval echo "${_su_value}"`
  if [ -z "${_su_value2}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_value _su_value2
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNotNull</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>value</emphasis> is <emphasis
#   role="strong">not</emphasis> <literal>null</literal>, or in shell terms not
#   a zero-length string. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNotNull()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_value=$1

  _su_value2=`eval echo "${_su_value}"`
  if [ -n "${_su_value2}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_value _su_value2
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function is functionally equivalent to
#   <function>assertEquals</function>.</para>
# </entry>
# </s:function>
#*/
assertSame()
{
  assertEquals "$@"
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNotSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are <emphasis role="strong">not</emphasis> equal to one another. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNotSame()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 3 ]; then
    _su_message=$1
    shift
  fi
  _su_expected=$1
  _su_actual=$2

  if [ "${_su_expected}" != "${_su_actual}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_expected _su_actual
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertTrue</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>condition</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that a given shell test condition is true. The message is
#   optional.</para>
#   <para>Testing whether something is true or false is easy enough by using
#   the assertEquals/assertNotSame functions. Shell supports much more
#   complicated tests though, and a means to support them was needed. As such,
#   this function tests that conditions are true or false through evaluation
#   rather than just looking for a true or false.</para>
#   <funcsynopsis>
#     The following test will succeed: <funcsynopsisinfo>assertTrue "[ 34 -gt 23 ]"</funcsynopsisinfo>
#     The folloing test will fail with a message: <funcsynopsisinfo>assertTrue "test failed" "[ -r '/non/existant/file' ]"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
assertTrue()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_condition=$1

  ( eval ${_su_condition} ) >/dev/null 2>&1
  if [ $? -eq ${SHUNIT_TRUE} ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_condition
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertFalse</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>condition</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that a given shell test condition is false. The message is
#   optional.</para>
#   <para>Testing whether something is true or false is easy enough by using
#   the assertEquals/assertNotSame functions. Shell supports much more
#   complicated tests though, and a means to support them was needed. As such,
#   this function tests that conditions are true or false through evaluation
#   rather than just looking for a true or false.</para>
#   <funcsynopsis>
#     The following test will succeed: <funcsynopsisinfo>assertFalse "[ 'apples' = 'oranges' ]"</funcsynopsisinfo>
#     The folloing test will fail with a message: <funcsynopsisinfo>assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
assertFalse()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_condition=$1

  ( eval ${_su_condition} ) >/dev/null 2>&1
  if [ $? -eq ${SHUNIT_FALSE} ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_condition
}

#-----------------------------------------------------------------------------
# failure functions
#

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>fail</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test immediately, with the optional message.</para>
# </entry>
# </s:function>
#*/
fail()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _shunit_testFailed "${@:-}"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failNotEquals</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are <emphasis role="strong">not</emphasis>
#   equal to one another. The message is optional.</para>
# </entry>
# </s:function>
#*/
failNotEquals()
{
  assertEquals "$@"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
failSame()
{
  assertNotSame "$@"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failNotSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
failNotSame()
{
  assertEquals "$@"
}

#-----------------------------------------------------------------------------
# skipping functions
#

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>startSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function forces the remaining assert and fail functions to be
#   "skipped", i.e. they will have no effect. Each function skipped will be
#   recorded so that the total of asserts and fails will not be altered.</para>
# </entry>
# </s:function>
#*/
startSkipping()
{
  __shunit_skip=${SHUNIT_TRUE}
}

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>endSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function returns calls to the assert and fail functions to their
#   default behavior, i.e. they will be called.</para>
# </entry>
# </s:function>
#*/
endSkipping()
{
  __shunit_skip=${SHUNIT_FALSE}
}

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>boolean</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>isSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function returns the state of skipping.</para>
# </entry>
# </s:function>
#*/
isSkipping()
{
  return ${__shunit_skip}
}

#-----------------------------------------------------------------------------
# suite functions
#

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>suite</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be optionally overridden by the user in their test
#   suite.</para>
#   <para>If this function exists, it will be called when
#   <command>shunit2</command> is sourced. If it does not exist,
#   <command>shunit2</command> will search the parent script for all functions
#   beginning with the word <literal>test</literal>, and they will be added
#   dynamically to the test suite.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# suite() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>suite_addTest</function></funcdef>
#       <paramdef>string <parameter>function</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function adds a function name to the list of tests scheduled for
#   execution as part of this test suite. This function should only be called
#   from within the <function>suite()</function> function.</para>
# </entry>
# </s:function>
#*/
suite_addTest()
{
  _su_func=$1

  __shunit_suite="${__shunit_suite:+${__shunit_suite} }${_su_func}"

  unset _su_func
}

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>oneTimeSetUp</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called once before any tests are
#   run. It is useful to prepare a common environment for all tests.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# oneTimeSetUp() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>oneTimeTearDown</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called once after all tests are
#   completed. It is useful to clean up the environment after all tests.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# oneTimeTearDown() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>setUp</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called before each test is run.
#   It is useful to reset the environment before each test.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# setUp() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>tearDown</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called after each test completes.
#   It is useful to clean up the environment after each test.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# tearDown() { :; }

#------------------------------------------------------------------------------
# internal shUnit functions
#

_shunit_cleanup()
{
  name=$1

  case ${name} in
    EXIT) signal=0 ;;
    INT) signal=2 ;;
    TERM) signal=15 ;;
  esac

  # do our work
  rm -fr "${__shunit_tmpDir}"

  # exit for all non-EXIT signals
  if [ ${name} != 'EXIT' ]; then
    echo "trapped and now handling the ${name} signal" >&2
    _shunit_generateReport
    # disable EXIT trap
    trap 0
    # add 127 to signal and exit
    signal=`expr ${signal} + 127`
    exit ${signal}
  fi
}

_shunit_execSuite()
{
  echo '#'
  echo '# Performing tests'
  echo '#'
  for _su_func in ${__shunit_suite}; do
    # disable skipping
    endSkipping

    # execute the per-test setup function
    setUp

    # execute the test
    echo "${_su_func}"
    eval ${_su_func}

    # execute the per-test tear-down function
    tearDown
  done

  unset _su_func
}

_shunit_functionExists()
{
  _su__func=$1
  type ${_su__func} 2>/dev/null |grep "is a function$" >/dev/null
  _su__return=$?
  unset _su__func
  return ${_su__return}
}

_shunit_generateReport()
{
  _su__awk='{printf("%4d %3.0f%%", $1, $1*100/$2)}'
  if [ ${__shunit_testsTotal} -gt 0 ]; then
    _su__passed=`echo ${__shunit_testsPassed} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__failed=`echo ${__shunit_testsFailed} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__skipped=`echo ${__shunit_testsSkipped} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__total=`echo ${__shunit_testsTotal} 100 |\
        awk '{printf("%4d %3d%%", $1, $2)}'`
  else
    _su__passed=`echo 0 0 |awk '{printf("%4d %3d%%", $1, $2)}'`
    _su__failed=${_su__passed}
    _su__skipped=${_su__passed}
    _su__total=${_su__passed}
  fi

  cat <<EOF

#
# Test report
#
tests passed:  ${_su__passed}
tests failed:  ${_su__failed}
tests skipped: ${_su__skipped}
tests total:   ${_su__total}
EOF

  unset _su__awk _su__passed _su__failed _su__skipped _su__total
}

# this function is a cross-platform temporary directory creation tool. not all
# OSes have the mktemp function, so one is included here.
_shunit_mktempDir()
{
  # try the standard mktemp function
  ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ) && return

  # the standard mktemp didn't work.  doing our own.
  if [ -r '/dev/urandom' ]; then
    _su__random=`od -vAn -N4 -tx4 </dev/urandom |sed 's/^[^0-9a-f]*//'`
  elif [ -n "${RANDOM:-}" ]; then
    # $RANDOM works
    _su__random=${RANDOM}${RANDOM}${RANDOM}$$
  else
    # $RANDOM doesn't work
    _su__date=`date '+%Y%m%d%H%M%S'`
    _su__random=`expr ${_su__date} / $$`
  fi

  _su__tmpDir="${TMPDIR-/tmp}/shunit.${_su__random}"
  ( umask 077 && mkdir "${_su__tmpDir}" ) || {
    echo 'shUnit:FATAL could not create temporary directory! exiting' >&2
    exit 1
  }

  echo ${_su__tmpDir}
  unset _su__date _su__random _su__tmpDir
}

# this function is here to work around issues in Cygwin
_shunit_mktempFunc()
{
  for _su__func in oneTimeSetUp oneTimeTearDown setUp tearDown suite; do
    _su__file="${__shunit_tmpDir}/${_su__func}"
    cat <<EOF >"${_su__file}"
#! /bin/sh
exit 0
EOF
    chmod +x "${_su__file}"
  done

  unset _su__file
}

_shunit_shouldSkip()
{
  [ ${__shunit_skip} -eq ${SHUNIT_FALSE} ] && return ${SHUNIT_FALSE}
  _shunit_testSkipped
}

_shunit_testPassed()
{
  __shunit_testsPassed=`expr ${__shunit_testsPassed} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
}

_shunit_testFailed()
{
  [ $# -eq 1 ] && _su__msg=$1

  __shunit_testsFailed=`expr ${__shunit_testsFailed} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`

  [ -z "${_su__msg:-}" ] && _su__msg='failed'
  echo "${__SHUNIT_ASSERT_MSG_PREFIX} ${_su__msg}" >&2
  unset _su__msg
}

_shunit_testSkipped()
{
  __shunit_testsSkipped=`expr ${__shunit_testsSkipped} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
}

#------------------------------------------------------------------------------
# main
#

# create a temporary storage location
__shunit_tmpDir=`_shunit_mktempDir`

# setup traps to clean up after ourselves
trap '_shunit_cleanup EXIT' 0
trap '_shunit_cleanup INT' 2
trap '_shunit_cleanup TERM' 15

# create phantom functions to work around issues with Cygwin
_shunit_mktempFunc
PATH="${__shunit_tmpDir}:${PATH}"

# execute the oneTimeSetUp function (if it exists)
#_shunit_functionExists oneTimeSetUp && oneTimeSetUp
oneTimeSetUp

# deprecated: execute the suite function defined in the parent test script
suite

# if no suite function was defined, dynamically build a list of functions
if [ -z "${__shunit_suite}" ]; then
  funcs=`grep "^[ \t]*test[A-Za-z0-9_]* *()" $0 |sed 's/[^A-Za-z0-9_]//g'`
  for func in ${funcs}; do
    suite_addTest ${func}
  done
fi

# execute the tests
_shunit_execSuite

# execute the oneTimeTearDown function (if it exists)
oneTimeTearDown

# generate report
_shunit_generateReport

# restore the previous set of shell flags
for _shunit_shellFlag in ${__SHUNIT_SHELL_FLAGS}; do
  echo ${__shunit_oldShellFlags} |grep ${_shunit_shellFlag} >/dev/null \
    || set +${_shunit_shellFlag}
done
unset _shunit_shellFlag

#/**
# </s:shelldoc>
#*/
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/log4sh.properties�����������������������������������������0000644�0000765�0000024�00000000455�11232747060�023473� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Set root logger level to INFO and its only appender to A1
log4sh.rootLogger=INFO, A1

# A1 is set to be a ConsoleAppender.
log4sh.appender.A1=ConsoleAppender

# A1 uses a PatternLayout.
log4sh.appender.A1.layout=PatternLayout
log4sh.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/migrations/�����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022333� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/migrations/001��������������������������������������������0000644�0000765�0000024�00000000141�11232765675�022560� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/lib/migrate

cd webapp

python manage.py syncdb
python manage.py migrate

cd ../


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/migrations/002��������������������������������������������0000644�0000765�0000024�00000000626�11233004154�022544� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# migration 002 -- need to grab the old librelist deploy
# copy the archives and database over

if [ -e ~/deploy/librelist ]
then
    logger_info "Restoring the old run queues and data"
    rsync -av ~/deploy/librelist/run .

    logger_info "Restoring the old archives data"
    rsync -av ~/deploy/librelist/app/data/archive app/data
else
    logger_info "No old librelist deploy, no need to run 002."
fi
����������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/migrations/003��������������������������������������������0000644�0000765�0000024�00000000551�11233230670�022546� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������tar -xjvf ~/deploy/data/spamdb.tar.bz2

sb_mboxtrain.py -d run/spamdb -f -g mbox ; rm mbox
sb_mboxtrain.py -d run/spamdb -f -g spam ; rm spam
sb_mboxtrain.py -d run/spamdb -f -g spam-db ; rm spam-db
sb_mboxtrain.py -d run/spamdb -f -g spam-mbox ; rm spam-mbox

echo "SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}" >> config/settings.py


�������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/migrations/004��������������������������������������������0000644�0000765�0000024�00000000231�11233530034�022537� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# migrates the archives to create the json archives as well

pushd ~/deploy/ENV
easy_install simplejson
popd

python deploy/migrations/json_convert.py


�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/rollback��������������������������������������������������0000644�0000765�0000024�00000000516�11233230670�021662� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup
stop $TARGET

TS=`date +%F.%T`

mv $TARGET $FAILS/$TS
logger_info "Failure was placed in $TS, if the backup fails go there."

rollback 0B $BACKUP $TARGET

logger_info "Rollback complete and failure placed in $TS"
logger_warn "System is now stopped, you should fix it."
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/scripts/��������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021646� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/deploy/scripts/json_convert.py�����������������������������������0000644�0000765�0000024�00000001442�11233521664�024725� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys
sys.path.append(".")

from lamson.mail import MailRequest, MailResponse
from lamson.queue import Queue
import config.testing
from app.model import archive
import os


def convert_queue(arg, dirname, names):
    if dirname.endswith("new"):
        print dirname, names

        jpath = dirname + "/../../json"
        if not os.path.exists(jpath):
            os.mkdir(jpath)

        for key in names:
            json_file = key + ".json"
            json_archive = os.path.join(jpath, json_file)

            fpath = os.path.join(dirname, key)
            msg = MailRequest('librelist.com', None, None, open(fpath).read())
            f = open(json_archive, "w")
            f.write(archive.to_json(msg.base))
            f.close()

os.path.walk("app/data/archives", convert_queue, None)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/lib/�������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017431� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/lib/__init__.py��������������������������������������������������0000644�0000765�0000024�00000000000�11230225103�021505� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/lib/metaphone.py�������������������������������������������������0000644�0000765�0000024�00000034673�11230232035�021757� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!python
#coding= latin-1
# This script implements the Double Metaphone algorythm (c) 1998, 1999 by Lawrence Philips
# it was translated to Python from the C source written by Kevin Atkinson (http://aspell.net/metaphone/)
# By Andrew Collins - January 12, 2007 who claims no rights to this work
# http://atomboy.isa-geek.com:8080/plone/Members/acoil/programing/double-metaphone
# Tested with Pyhon 2.4.3
# Updated Feb 14, 2007 - Found a typo in the 'gh' section
# Updated Dec 17, 2007 - Bugs fixed in 'S', 'Z', and 'J' sections. Thanks Chris Leong!
def dm(st) :
	"""dm(string) -> (string, string or None)
	returns the double metaphone codes for given string - always a tuple
	there are no checks done on the input string, but it should be a single	word or name."""
	vowels = ['A', 'E', 'I', 'O', 'U', 'Y']
	st = st.decode('ascii', 'ignore')
	st = st.upper() # st is short for string. I usually prefer descriptive over short, but this var is used a lot!
	is_slavo_germanic = (st.find('W') > -1 or st.find('K') > -1 or st.find('CZ') > -1 or st.find('WITZ') > -1)
	length = len(st)
	first = 2
	st = '-' * first + st + '------'  # so we can index beyond the begining and end of the input string
	last = first + length -1
	pos = first # pos is short for position
	pri = sec = '' # primary and secondary metaphone codes
	#skip these silent letters when at start of word
	if st[first:first+2] in ["GN", "KN", "PN", "WR", "PS"] :
		pos += 1
	# Initial 'X' is pronounced 'Z' e.g. 'Xavier'
	if st[first] == 'X' :
		pri = sec = 'S' #'Z' maps to 'S'
		pos += 1
	# main loop through chars in st
	while pos <= last :
		#print str(pos) + '\t' + st[pos]
		ch = st[pos] # ch is short for character
		# nxt (short for next characters in metaphone code) is set to  a tuple of the next characters in
		# the primary and secondary codes and how many characters to move forward in the string.
		# the secondary code letter is given only when it is different than the primary.
		# This is just a trick to make the code easier to write and read.
		nxt = (None, 1) # default action is to add nothing and move to next char
		if ch in vowels :
			nxt = (None, 1)
			if pos == first : # all init vowels now map to 'A'
				nxt = ('A', 1)
		elif ch == 'B' :
			#"-mb", e.g", "dumb", already skipped over... see 'M' below
			if st[pos+1] == 'B' :
				nxt = ('P', 2)
			else :
				nxt = ('P', 1)
		elif ch == 'C' :
			# various germanic
			if (pos > first and st[pos-2] in vowels and st[pos-1:pos+1] == 'ACH' and \
			   (st[pos+2] not in ['I', 'E'] or st[pos-2:pos+4] in ['BACHER', 'MACHER'])) :
				nxt = ('K', 2)
			# special case 'CAESAR'
			elif pos == first and st[first:first+6] == 'CAESAR' :
				nxt = ('S', 2)
			elif st[pos:pos+4] == 'CHIA' : #italian 'chianti'
				nxt = ('K', 2)
			elif st[pos:pos+2] == 'CH' :
				# find 'michael'
				if pos > first and st[pos:pos+4] == 'CHAE' :
					nxt = ('K', 'X', 2)
				elif pos == first and (st[pos+1:pos+6] in ['HARAC', 'HARIS'] or \
				   st[pos+1:pos+4] in ["HOR", "HYM", "HIA", "HEM"]) and st[first:first+5] != 'CHORE' :
					nxt = ('K', 2)
				#germanic, greek, or otherwise 'ch' for 'kh' sound
				elif st[first:first+4] in ['VAN ', 'VON '] or st[first:first+3] == 'SCH' \
				   or st[pos-2:pos+4] in ["ORCHES", "ARCHIT", "ORCHID"] \
				   or st[pos+2] in ['T', 'S'] \
				   or ((st[pos-1] in ["A", "O", "U", "E"] or pos == first) \
				   and st[pos+2] in ["L", "R", "N", "M", "B", "H", "F", "V", "W"]) :
					nxt = ('K', 1)
				else :
					if pos == first :
						if st[first:first+2] == 'MC' :
							nxt = ('K', 2)
						else :
							nxt = ('X', 'K', 2)
					else :
						nxt = ('X', 2)
			#e.g, 'czerny'
			elif st[pos:pos+2] == 'CZ' and st[pos-2:pos+2] != 'WICZ' :
				nxt = ('S', 'X', 2)
			#e.g., 'focaccia'
			elif st[pos+1:pos+4] == 'CIA' :
				nxt = ('X', 3)
			#double 'C', but not if e.g. 'McClellan'
			elif st[pos:pos+2] == 'CC' and not (pos == (first +1) and st[first] == 'M') :
				#'bellocchio' but not 'bacchus'
				if st[pos+2] in ["I", "E", "H"] and st[pos+2:pos+4] != 'HU' :
					#'accident', 'accede' 'succeed'
					if (pos == (first +1) and st[first] == 'A') or \
					   st[pos-1:pos+4] in ['UCCEE', 'UCCES'] :
						nxt = ('KS', 3)
					#'bacci', 'bertucci', other italian
					else:
						nxt = ('X', 3)
				else :
					nxt = ('K', 2)
			elif st[pos:pos+2] in ["CK", "CG", "CQ"] :
				nxt = ('K', 'K', 2)
			elif st[pos:pos+2] in ["CI", "CE", "CY"] :
				#italian vs. english
				if st[pos:pos+3] in ["CIO", "CIE", "CIA"] :
					nxt = ('S', 'X', 2)
				else :
					nxt = ('S', 2)
			else : 
				#name sent in 'mac caffrey', 'mac gregor
				if st[pos+1:pos+3] in [" C", " Q", " G"] :
					nxt = ('K', 3)
				else :
					if st[pos+1] in ["C", "K", "Q"] and st[pos+1:pos+3] not in ["CE", "CI"] :
						nxt = ('K', 2)
					else : # default for 'C'
						nxt = ('K', 1)
		elif ch == u'' : # will never get here with st.encode('ascii', 'replace') above
			nxt = ('S', 1)
		elif ch == 'D' :
			if st[pos:pos+2] == 'DG' :
				if st[pos+2] in ['I', 'E', 'Y'] : #e.g. 'edge'
					nxt = ('J', 3)
				else :
					nxt = ('TK', 2)
			elif st[pos:pos+2] in ['DT', 'DD'] :
				nxt = ('T', 2)
			else :
				nxt = ('T', 1)
		elif ch == 'F' :
			if st[pos+1] == 'F' :
				nxt = ('F', 2)
			else :
				nxt = ('F', 1)
		elif ch == 'G' :
			if st[pos+1] == 'H' :
				if pos > first and st[pos-1] not in vowels :
					nxt = ('K', 2)
				elif pos < (first + 3) :
					if pos == first : #'ghislane', ghiradelli
						if st[pos+2] == 'I' :
							nxt = ('J', 2)
						else :
							nxt = ('K', 2)
				#Parker's rule (with some further refinements) - e.g., 'hugh'
				elif (pos > (first + 1) and st[pos-2] in ['B', 'H', 'D'] ) \
				   or (pos > (first + 2) and st[pos-3] in ['B', 'H', 'D'] ) \
				   or (pos > (first + 3) and st[pos-3] in ['B', 'H'] ) :
					nxt = (None, 2)
				else : 
					# e.g., 'laugh', 'McLaughlin', 'cough', 'gough', 'rough', 'tough'
					if pos > (first + 2) and st[pos-1] == 'U' \
					   and st[pos-3] in ["C", "G", "L", "R", "T"] :
						nxt = ('F', 2)
					else :
						if pos > first and st[pos-1] != 'I' :
							nxt = ('K', 2)
			elif st[pos+1] == 'N' :
				if pos == (first +1) and st[first] in vowels and not is_slavo_germanic :
					nxt = ('KN', 'N', 2)
				else :
					# not e.g. 'cagney'
					if st[pos+2:pos+4] != 'EY' and st[pos+1] != 'Y' and not is_slavo_germanic :
						nxt = ('N', 'KN', 2)
					else :
						nxt = ('KN', 2)
			# 'tagliaro'
			elif st[pos+1:pos+3] == 'LI' and not is_slavo_germanic :
				nxt = ('KL', 'L', 2)
			# -ges-,-gep-,-gel-, -gie- at beginning
			elif pos == first and (st[pos+1] == 'Y' \
			   or st[pos+1:pos+3] in ["ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER"]) :
				nxt = ('K', 'J', 2)
			# -ger-,  -gy-
			elif (st[pos+1:pos+2] == 'ER' or st[pos+1] == 'Y') \
			   and st[first:first+6] not in ["DANGER", "RANGER", "MANGER"] \
			   and st[pos-1] not in ['E', 'I'] and st[pos-1:pos+2] not in ['RGY', 'OGY'] :
				nxt = ('K', 'J', 2)
			# italian e.g, 'biaggi'
			elif st[pos+1] in ['E', 'I', 'Y'] or st[pos-1:pos+3] in ["AGGI", "OGGI"] :
				# obvious germanic
				if st[first:first+4] in ['VON ', 'VAN '] or st[first:first+3] == 'SCH' \
				   or st[pos+1:pos+3] == 'ET' :
					nxt = ('K', 2)
				else :
					# always soft if french ending
					if st[pos+1:pos+5] == 'IER ' :
						nxt = ('J', 2)
					else :
						nxt = ('J', 'K', 2)
			elif st[pos+1] == 'G' :
				nxt = ('K', 2)
			else :
				nxt = ('K', 1)
		elif ch == 'H' :
			# only keep if first & before vowel or btw. 2 vowels
			if (pos == first or st[pos-1] in vowels) and st[pos+1] in vowels :
				nxt = ('H', 2)
			else : # (also takes care of 'HH')
				nxt = (None, 1)
		elif ch == 'J' :
			# obvious spanish, 'jose', 'san jacinto'
			if st[pos:pos+4] == 'JOSE' or st[first:first+4] == 'SAN ' :
				if (pos == first and st[pos+4] == ' ') or st[first:first+4] == 'SAN ' :
					nxt = ('H',)
				else :
					nxt = ('J', 'H')
			elif pos == first and st[pos:pos+4] != 'JOSE' :
				nxt = ('J', 'A') # Yankelovich/Jankelowicz
			else :
				# spanish pron. of e.g. 'bajador'
				if st[pos-1] in vowels and not is_slavo_germanic \
				   and st[pos+1] in ['A', 'O'] :
					nxt = ('J', 'H')
				else :
					if pos == last :
						nxt = ('J', ' ')
					else :
						if st[pos+1] not in ["L", "T", "K", "S", "N", "M", "B", "Z"] \
						   and st[pos-1] not in ["S", "K", "L"] :
							nxt = ('J',)
						else :
							nxt = (None, )
			if st[pos+1] == 'J' :
				nxt = nxt + (2,)
			else :
				nxt = nxt + (1,)
		elif ch == 'K' :
			if st[pos+1] == 'K' :
				nxt = ('K', 2)
			else :
				nxt = ('K', 1)
		elif ch == 'L' :
			if st[pos+1] == 'L' :
				# spanish e.g. 'cabrillo', 'gallegos'
				if (pos == (last - 2) and st[pos-1:pos+3] in ["ILLO", "ILLA", "ALLE"]) \
				   or (st[last-1:last+1] in ["AS", "OS"] or st[last] in ["A", "O"] \
				   and st[pos-1:pos+3] == 'ALLE') :
					nxt = ('L', ' ', 2)
				else :
					nxt = ('L', 2)
			else :
				nxt = ('L', 1)
		elif ch == 'M' :
			if st[pos+1:pos+4] == 'UMB' \
			   and (pos + 1 == last or st[pos+2:pos+4] == 'ER') \
			   or st[pos+1] == 'M' :
				nxt = ('M', 2)
			else :
				nxt = ('M', 1)
		elif ch == 'N' :
			if st[pos+1] == 'N' :
				nxt = ('N', 2)
			else :
				nxt = ('N', 1)
		elif ch == u'' :
			nxt = ('N', 1)
		elif ch == 'P' :
			if st[pos+1] == 'H' :
				nxt = ('F', 2)
			elif st[pos+1] in ['P', 'B'] : # also account for "campbell", "raspberry"
				nxt = ('P', 2)
			else :
				nxt = ('P', 1)
		elif ch == 'Q' :
			if st[pos+1] == 'Q' :
				nxt = ('K', 2)
			else :
				nxt = ('K', 1)
		elif ch == 'R' :
			# french e.g. 'rogier', but exclude 'hochmeier'
			if pos == last and not is_slavo_germanic \
			   and st[pos-2:pos] == 'IE' and st[pos-4:pos-2] not in ['ME', 'MA'] :
				nxt = ('', 'R')
			else :
				nxt = ('R',)
			if st[pos+1] == 'R' :
				nxt = nxt + (2,)
			else :
				nxt = nxt + (1,)
		elif ch == 'S' :
			# special cases 'island', 'isle', 'carlisle', 'carlysle'
			if st[pos-1:pos+2] in ['ISL', 'YSL'] :
				nxt = (None, 1)
			# special case 'sugar-'
			elif pos == first and st[first:first+5] == 'SUGAR' :
				nxt =('X', 'S', 1)
			elif st[pos:pos+2] == 'SH' :
				# germanic
				if st[pos+1:pos+5] in ["HEIM", "HOEK", "HOLM", "HOLZ"] :
					nxt = ('S', 2)
				else :
					nxt = ('X', 2)
			# italian & armenian
			elif st[pos:pos+3] in ["SIO", "SIA"] or st[pos:pos+4] == 'SIAN' :
				if not is_slavo_germanic :
					nxt = ('S', 'X', 3)
				else :
					nxt = ('S', 3)
			# german & anglicisations, e.g. 'smith' match 'schmidt', 'snider' match 'schneider'
			# also, -sz- in slavic language altho in hungarian it is pronounced 's'
			elif (pos == first and st[pos+1] in ["M", "N", "L", "W"]) or st[pos+1] == 'Z' :
				nxt = ('S', 'X')
				if st[pos+1] == 'Z' :
					nxt = nxt + (2,)
				else :
					nxt = nxt + (1,)
			elif st[pos+2:pos+4] == 'SC' :
				# Schlesinger's rule
				if st[pos+2] == 'H' :
					# dutch origin, e.g. 'school', 'schooner'
					if st[pos+3:pos+5] in ["OO", "ER", "EN", "UY", "ED", "EM"] :
						# 'schermerhorn', 'schenker'
						if st[pos+3:pos+5] in ['ER', 'EN'] :
							nxt = ('X', 'SK', 3)
						else :
							nxt = ('SK', 3)
					else :
						if pos == first and st[first+3] not in vowels and st[first+3] != 'W' :
							nxt = ('X', 'S', 3)
						else :
							nxt = ('X', 3)
				elif st[pos+2] in ['I', 'E', 'Y'] :
					nxt = ('S', 3)
				else :
					nxt = ('SK', 3)
			# french e.g. 'resnais', 'artois'
			elif pos == last and st[pos-2:pos] in ['AI', 'OI'] :
				nxt = ('', 'S', 1)
			else :
				nxt = ('S',)
				if st[pos+1] in ['S', 'Z'] :
					nxt = nxt + (2,)
				else :
					nxt = nxt + (1,)
		elif ch == 'T' :
			if st[pos:pos+4] == 'TION' :
				nxt = ('X', 3)
			elif st[pos:pos+3] in ['TIA', 'TCH'] :
				nxt = ('X', 3)
			elif st[pos:pos+2] == 'TH' or st[pos:pos+3] == 'TTH' :
				# special case 'thomas', 'thames' or germanic
				if st[pos+2:pos+4] in ['OM', 'AM'] or st[first:first+4] in ['VON ', 'VAN '] \
				   or st[first:first+3] == 'SCH' :
					nxt = ('T', 2)
				else :
					nxt = ('0', 'T', 2)
			elif st[pos+1] in ['T', 'D'] :
				nxt = ('T', 2)
			else :
				nxt = ('T', 1)
		elif ch == 'V' :
			if st[pos+1] == 'V' :
				nxt = ('F', 2)
			else :
				nxt = ('F', 1)
		elif ch == 'W' :
			# can also be in middle of word
			if st[pos:pos+2] == 'WR' :
				nxt = ('R', 2)
			elif pos == first and st[pos+1] in vowels or st[pos:pos+2] == 'WH' :
				# Wasserman should match Vasserman
				if st[pos+1] in vowels :
					nxt = ('A', 'F', 1)
				else :
					nxt = ('A', 1)
			# Arnow should match Arnoff
			elif (pos == last and st[pos-1] in vowels) \
			   or st[pos-1:pos+5] in ["EWSKI", "EWSKY", "OWSKI", "OWSKY"] \
			   or st[first:first+3] == 'SCH' :
				nxt = ('', 'F', 1)
			# polish e.g. 'filipowicz'
			elif st[pos:pos+4] in ["WICZ", "WITZ"] :
				nxt = ('TS', 'FX', 4)
			else : # default is to skip it
				nxt = (None, 1)
		elif ch == 'X' :
			# french e.g. breaux
			nxt = (None,)
			if not(pos == last and (st[pos-3:pos] in ["IAU", "EAU"] \
			   or st[pos-2:pos] in ['AU', 'OU'])):
				nxt = ('KS',)
			if st[pos+1] in ['C', 'X'] :
				nxt = nxt + (2,)
			else :
				nxt = nxt + (1,)
		elif ch == 'Z' :
			# chinese pinyin e.g. 'zhao'
			if st[pos+1] == 'H' :
				nxt = ('J',)
			elif st[pos+1:pos+3] in ["ZO", "ZI", "ZA"] \
			   or (is_slavo_germanic and pos > first and st[pos-1] != 'T') :
				nxt = ('S', 'TS')
			else :
				nxt = ('S',)
			if st[pos+1] == 'Z' :
				nxt = nxt + (2,)
			else :
				nxt = nxt + (1,)
		# ----------------------------------
		# --- end checking letters------
		# ----------------------------------
		#print str(nxt)
		if len(nxt) == 2 :
			if nxt[0] :
				pri += nxt[0]
				sec += nxt[0]
			pos += nxt[1]
		elif len(nxt) == 3 :
			if nxt[0] :
				pri += nxt[0]
			if nxt[1] :
				sec += nxt[1]
			pos += nxt[2]
	if pri == sec :
		return (pri, None)
	else :
		return (pri, sec)

if __name__ == '__main__' :
	names = {'maurice':'MRS','aubrey':'APR','cambrillo':'KMPR','heidi':'HT','katherine':'K0RN,KTRN',\
		     'catherine':'K0RN,KTRN','richard':'RXRT,RKRT','bob':'PP','eric':'ARK','geoff':'JF,KF',\
			 'dave':'TF','ray':'R','steven':'STFN','bryce':'PRS','randy':'RNT','bryan':'PRN',\
			 'brian':'PRN','otto':'AT','auto':'AT', 'maisey':'MS, None', 'zhang':'JNK, None', 'solilijs':'SLLS, None'}
	for name in names.keys() :
		print name + '\t-->\t' + str(dm(name)) + '\t(' +names[name] + ')'
���������������������������������������������������������������������lamson-1.0pre11/examples/librelist/muttrc�����������������������������������������������������������0000644�0000765�0000024�00000000345�11225417542�020121� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/README�����������������������������������������������������������0000644�0000765�0000024�00000000175�11226301713�017532� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������This is the Lamson mailing list sample code.  It is more complex than
examples/osb because it uses Django at the same time.

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/�����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020025� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/bounce.msg�������������������������������������������������0000600�0000765�0000024�00000006143�11226434474�022003� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������From MAILER-DAEMON  Tue Jul  7 22:47:39 2009
Return-Path: <>
X-Original-To: zedshaw@zedshaw.com
Delivered-To: zedshaw@zedshaw.com
Received: by mail.zedshaw.com (Postfix)
	id 270231E8B13; Tue,  7 Jul 2009 22:47:39 -0700 (PDT)
Date: Tue,  7 Jul 2009 22:47:39 -0700 (PDT)
From: MAILER-DAEMON@zedshaw.com (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: zedshaw@zedshaw.com
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
	boundary="C99B61E8B12.1247032059/mail.zedshaw.com"
Message-Id: <20090708054739.270231E8B13@mail.zedshaw.com>
Status: RO

This is a MIME-encapsulated message.

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Notification
Content-Type: text/plain; charset=us-ascii

This is the mail system at host mail.zedshaw.com.

I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.

For further assistance, please send mail to postmaster.

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

                   The mail system

<asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com>: host
    gmail-smtp-in.l.google.com[209.85.210.17] said: 550-5.1.1 The email account
    that you tried to reach does not exist. Please try 550-5.1.1
    double-checking the recipient's email address for typos or 550-5.1.1
    unnecessary spaces. Learn more at                              550 5.1.1
    http://mail.google.com/support/bin/answer.py?answer=6596 17si20661415yxe.22
    (in reply to RCPT TO command)

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Delivery report
Content-Type: message/delivery-status

Reporting-MTA: dns; mail.zedshaw.com
X-Postfix-Queue-ID: C99B61E8B12
X-Postfix-Sender: rfc822; zedshaw@zedshaw.com
Arrival-Date: Tue,  7 Jul 2009 22:47:35 -0700 (PDT)

Final-Recipient: rfc822; asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
Action: failed
Status: 5.1.1
Remote-MTA: dns; gmail-smtp-in.l.google.com
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does
    not exist. Please try 550-5.1.1 double-checking the recipient's email
    address for typos or 550-5.1.1 unnecessary spaces. Learn more at
    550 5.1.1 http://mail.google.com/support/bin/answer.py?answer=6596
    17si20661415yxe.22

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Undelivered Message
Content-Type: message/rfc822

Return-Path: <zedshaw@zedshaw.com>
Received: by mail.zedshaw.com (Postfix, from userid 1000)
	id C99B61E8B12; Tue,  7 Jul 2009 22:47:35 -0700 (PDT)
Date: Tue, 7 Jul 2009 22:47:35 -0700
From: "Zed A. Shaw" <zedshaw@zedshaw.com>
To: asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
Subject: test bounce
Message-ID: <20090708054735.GC7578@zedshaw>
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
User-Agent: Mutt/1.5.18 (2008-05-17)

If this is a real address, then sorry.  I'm trying to get some bounces.

-- 
Zed A. Shaw
http://zedshaw.com/

--C99B61E8B12.1247032059/mail.zedshaw.com--
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/handlers/��������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021625� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/handlers/__init__.py���������������������������������������0000644�0000765�0000024�00000000000�11225712150�023710� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/handlers/admin_tests.py������������������������������������0000644�0000765�0000024�00000003201�11233526001�024466� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from config import settings
import time
from app.model import archive, confirmation


queue_path = archive.store_path('test.list', 'queue')
sender = "sender-%s@sender.com" % time.time()
host = "librelist.com"
list_name = "test.list"
list_addr = "test.list@%s" % host
client = RouterConversation(sender, 'Admin Tests')

def setup():
    clear_queue("run/posts")
    clear_queue("run/spam")

def test_new_user_subscribes_with_invalid_name():
    client.begin()

    client.say('test-list@%s' % host, "I can't read!", 'noreply')
    client.say('test=list@%s' % host, "I can't read!", 'noreply')
    clear_queue()

    client.say('unbounce@%s' % host, "I have two email addresses!")
    assert not delivered('noreply')
    assert not delivered('unbounce')

    client.say('noreply@%s' % host, "Dumb dumb.")
    assert not delivered('noreply')

def test_new_user_subscribes():
    client.begin()
    msg = client.say(list_addr, "Hey I was wondering how to fix this?",
                     list_name + '-confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')
    clear_queue()


def test_existing_user_unsubscribes():
    test_new_user_subscribes()
    msg = client.say(list_name + "-unsubscribe@%s" % host, "I would like to unsubscribe.", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed yes I want out.', 'noreply')

def test_existing_user_posts_message():
    test_new_user_subscribes()
    msg = client.say(list_addr, "Howdy folks, I was wondering what this is?",
                     list_addr)
    # make sure it gets archived
    assert delivered(list_addr, to_queue=queue(queue_path))


�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/handlers/bounce_tests.py�����������������������������������0000644�0000765�0000024�00000010625�11242535527�024676� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest
from lamson.routing import Router
from app.handlers.admin import module_and_to
from app.model import mailinglist
from handlers import admin_tests
from email.utils import parseaddr
from lamson import bounce
from config import settings

sender = admin_tests.sender
list_addr = admin_tests.list_addr
client = admin_tests.client

def setup():
    clear_queue(queue_dir=settings.BOUNCE_ARCHIVE)

def create_bounce(To, From):
    msg = MailRequest("fakepeer", From, To, open("tests/bounce.msg").read())
    assert msg.is_bounce()

    msg.bounce.final_recipient = From
    msg.bounce.headers['Final-Recipient'] = From
    msg.bounce.original['from'] = From
    msg.bounce.original['to'] = To
    msg.bounce.original.To = set([To])
    msg.bounce.original.From = From

    return msg


def test_hard_bounce_disables_user():
    # get them into a posting state
    admin_tests.test_existing_user_posts_message()
    assert_in_state('app.handlers.admin', list_addr, sender, 'POSTING')
    clear_queue()
    assert mailinglist.find_subscriptions(sender, list_addr)

    # force them to HARD bounce
    msg = create_bounce(list_addr, sender)

    Router.deliver(msg)
    assert_in_state('app.handlers.admin', list_addr, sender, 'BOUNCING')
    assert_in_state('app.handlers.bounce', list_addr, sender, 'BOUNCING')
    assert not delivered('unbounce'), "A HARD bounce should be silent."
    assert_equal(len(queue(queue_dir=settings.BOUNCE_ARCHIVE).keys()), 1)
    assert not mailinglist.find_subscriptions(sender, list_addr)

    # make sure that any attempts to post return a "you're bouncing dude" message
    unbounce = client.say(list_addr, 'So anyway as I was saying.', 'unbounce')
    assert_in_state('app.handlers.admin', list_addr, sender, 'BOUNCING')
   
    # now have them try to unbounce
    msg = client.say(unbounce['from'], "Please put me back on, I'll be good.",
                     'unbounce-confirm')

    # handle the bounce confirmation
    client.say(msg['from'], "Confirmed to unbounce.", 'noreply')

    # alright they should be in the unbounce state for the global bounce handler
    assert_in_state('app.handlers.bounce', list_addr, sender,
                    'UNBOUNCED')

    # and they need to be back to POSTING for regular operations 
    assert_in_state('app.handlers.admin', list_addr, sender, 'POSTING')
    assert mailinglist.find_subscriptions(sender, list_addr)

    # and make sure that only the original bounce is in the bounce archive
    assert_equal(len(queue(queue_dir=settings.BOUNCE_ARCHIVE).keys()), 1)

def test_soft_bounce_tells_them():
    setup()

    # get them into a posting state
    admin_tests.test_existing_user_posts_message()
    assert_in_state('app.handlers.admin', list_addr, sender, 'POSTING')
    clear_queue()
    assert mailinglist.find_subscriptions(sender, list_addr)

    # force them to soft bounce
    msg = create_bounce(list_addr, sender)
    msg.bounce.primary_status = (3, bounce.PRIMARY_STATUS_CODES[u'3'])
    assert msg.bounce.is_soft()

    Router.deliver(msg)
    assert_in_state('app.handlers.admin', list_addr, sender, 'BOUNCING')
    assert_in_state('app.handlers.bounce', list_addr, sender, 'BOUNCING')
    assert delivered('unbounce'), "Looks like unbounce didn't go out."
    assert_equal(len(queue(queue_dir=settings.BOUNCE_ARCHIVE).keys()), 1)
    assert not mailinglist.find_subscriptions(sender, list_addr)

    # make sure that any attempts to post return a "you're bouncing dude" message
    unbounce = client.say(list_addr, 'So anyway as I was saying.', 'unbounce')
    assert_in_state('app.handlers.admin', list_addr, sender, 'BOUNCING')

    # now have them try to unbounce
    msg = client.say(unbounce['from'], "Please put me back on, I'll be good.",
                     'unbounce-confirm')

    # handle the bounce confirmation
    client.say(msg['from'], "Confirmed to unbounce.", 'noreply')

    # alright they should be in the unbounce state for the global bounce handler
    assert_in_state('app.handlers.bounce', list_addr, sender,
                    'UNBOUNCED')

    # and they need to be back to POSTING for regular operations 
    assert_in_state('app.handlers.admin', list_addr, sender, 'POSTING')
    assert mailinglist.find_subscriptions(sender, list_addr)

    # and make sure that only the original bounce is in the bounce archive
    assert_equal(len(queue(queue_dir=settings.BOUNCE_ARCHIVE).keys()), 1)


�����������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/lots_of_headers.msg����������������������������������������0000644�0000765�0000024�00000005247�11230507123�023667� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Received: from bazar2.conectiva.com.br (bazar2.conectiva.com.br [127.0.0.1])
	by bazar2.conectiva.com.br (Postfix) with ESMTP id D906B18AD6;
	Sun, 21 Jun 2009 17:42:54 -0300 (BRT)
X-Original-To: lua@bazar2.conectiva.com.br
Delivered-To: lua@bazar2.conectiva.com.br
Received: from smtp-out-01.simnet.is (smtp-out-01.simnet.is [194.105.231.27])
	by bazar2.conectiva.com.br (Postfix) with ESMTP id E2C9118A1B
	for <lua@bazar2.conectiva.com.br>; Sun, 21 Jun 2009 17:42:50 -0300 (BRT)
Authentication-Results: smtp-out-01.simnetpro.is;
	dkim=neutral (message not signed) header.i=none
X-SBRS: 0.8
X-IronPort-Anti-Spam-Filtered: true
X-IronPort-Anti-Spam-Result: Au4KAKo4PkrCaedFbGdsb2JhbACYXwsOCQcTswaECgU
X-IronPort-AV: E=Sophos;i="4.42,264,1243814400"; d="scan'208";a="222914853"
Received: from consumer-mta-01.simnet.is ([194.105.231.69])
	by smtp-out-01.simnetpro.is with ESMTP; 21 Jun 2009 20:42:38 +0000
Subject: Re: Problem Integrating Lua with Visual Studio 2005
From: =?ISO-8859-1?Q?Gabr=EDel?= "A." =?ISO-8859-1?Q?P=E9tursson?=
	<gabrielp@simnet.is>
To: Lua list <lua@bazar2.conectiva.com.br>
In-Reply-To: <dbaa5cbe0906211336uf58d390r94f5da3017d0fd5c@mail.gmail.com>
References: <dbaa5cbe0906211336uf58d390r94f5da3017d0fd5c@mail.gmail.com>
Content-Type: text/plain
Date: Sun, 21 Jun 2009 20:42:38 +0000
Message-Id: <1245616958.16548.2.camel@debian.lan>
Mime-Version: 1.0
X-Mailer: Evolution 2.26.1.1 
Content-Transfer-Encoding: 7bit
X-BeenThere: lua@bazar2.conectiva.com.br
X-Mailman-Version: 2.1.6
Precedence: list
Reply-To: Lua list <lua@bazar2.conectiva.com.br>
List-Id: Lua list <lua.bazar2.conectiva.com.br>
List-Unsubscribe: <http://bazar2.conectiva.com.br/mailman/listinfo/lua>,
	<mailto:lua-request@bazar2.conectiva.com.br?subject=unsubscribe>
List-Archive: <http://bazar2.conectiva.com.br/mailman/private/lua>
List-Post: <mailto:lua@bazar2.conectiva.com.br>
List-Help: <mailto:lua-request@bazar2.conectiva.com.br?subject=help>
List-Subscribe: <http://bazar2.conectiva.com.br/mailman/listinfo/lua>,
	<mailto:lua-request@bazar2.conectiva.com.br?subject=subscribe>
Sender: lua-bounces@bazar2.conectiva.com.br
Errors-To: lua-bounces@bazar2.conectiva.com.br
X-Spambayes-Classification: ham; 0.00

You may start by telling us what kind or errors you are experiencing.

On Sun, 2009-06-21 at 16:36 -0400, shanthan raj wrote:
> Hi,
> 
> I'm new to Lua. I want to develop a game using Lua. I'm planning to
> use Visual Studio 2005 for the development. I have downloaded [VS Lua
> Language Pack] (5.1) . When a run a simple program in Lua using Visual
> Studio, I'm getting some errors. May be I'm doing something wrong. Can
> anybody guide how to start with a simple "Hello World" program.
> 
> Thanks in Advance,
> 
> Raj
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/�����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021125� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/__init__.py������������������������������������������0000644�0000765�0000024�00000000000�11225417645�023223� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/archive_tests.py�������������������������������������0000644�0000765�0000024�00000003613�11251075175�024341� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest, MailResponse
from app.model import archive, mailinglist
import simplejson as json
import shutil

queue_path = archive.store_path('test.list', 'queue')
json_path = archive.store_path('test.list', 'json')

def setup():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def teardown():
    clear_queue(queue_path)
    shutil.rmtree(json_path)

def test_archive_enqueue():
    msg = MailResponse(From=u'"p\xf6stal Zed" <zedshaw@zedshaw.com>', 
                       To="test.list@librelist.com",
                       Subject="test message", Body="This is a test.")

    archive.enqueue('test.list', msg)
    archived = delivered('zedshaw', to_queue=queue(queue_path))
    assert archived, "Didn't get archived."
    as_string = str(archived)

    assert '-AT-' in str(archived), "Didn't get obfuscated"
    assert '<' in as_string and '"' in as_string and '>' in as_string, "Unicode email screwed up."



def test_white_list_cleanse():
    msg = MailRequest('fakepeer', None, None, open('tests/lots_of_headers.msg').read())
    resp = mailinglist.craft_response(msg, 'test.list', 'test.list@librelist.com')

    archive.white_list_cleanse(resp)
    
    for key in resp.keys():
        assert key in archive.ALLOWED_HEADERS

    assert '@' not in resp['from']
    assert str(resp)

def test_to_json():
    msg = MailRequest('fakeperr', None, None, open("tests/bounce.msg").read())

    resp = mailinglist.craft_response(msg, 'test.list', 'test.list@librelist.com')
    # attach an the message back but fake it as an image it'll be garbage
    resp.attach(filename="tests/bounce.msg", content_type="image/png", disposition="attachment")
    resp.to_message()  # prime the pump

    js = archive.to_json(resp.base)
    assert js

    rtjs = json.loads(js)
    assert rtjs
    assert rtjs['parts'][-1]['encoding']['format'] == 'base64'
���������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/bounce_tests.py��������������������������������������0000644�0000765�0000024�00000000623�11230006350�024153� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest
from app.model import bounce


def test_mail_to_you_is_bouncing():
    msg = MailRequest("fakepeer", None, None, open("tests/bounce.msg").read())
    assert msg.is_bounce()

    bounce_rep = bounce.mail_to_you_is_bouncing(msg)
    assert bounce_rep
    assert_equal(bounce_rep['to'], msg.bounce.final_recipient)

�������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/confirmation_tests.py��������������������������������0000644�0000765�0000024�00000001222�11306514030�025367� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson.mail import MailRequest, MailResponse
from app.model.confirmation import DjangoConfirmStorage
from mock import patch

user = "test_user@localhost"
list_name = "test_list_name"



def test_DjangoConfirmStorage():
    storage = DjangoConfirmStorage()
    storage.clear()

    storage.store(list_name, user, '123456', 'abcdefg')

    secret, pending_id = storage.get(list_name, user)
    assert_equal(secret, '123456')
    assert_equal(pending_id, 'abcdefg')

    storage.delete(list_name, user)

    secret, pending = storage.get(list_name, user)
    assert not secret
    assert not pending


������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/mailinglist_tests.py���������������������������������0000644�0000765�0000024�00000011210�11234145410�025213� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from app.model.mailinglist import *
from email.utils import parseaddr
from webapp.librelist.models import MailingList, Subscription
from lamson.mail import MailRequest, MailResponse
from lamson.testing import *

user_full_address = '"Zed A. Shaw" <zedshaw@zedshaw.com>'
user_name, user_address = parseaddr(user_full_address)
list_name = "test.lists"


def setup():
    MailingList.objects.all().delete()
    Subscription.objects.all().delete()


def test_create_list():
    mlist = create_list(list_name)
    assert mlist
    mlist_found = find_list(list_name)
    assert mlist_found
    assert_equal(mlist.name, mlist_found.name)

    # make sure create doesn't do it more than once
    create_list(list_name)
    assert_equal(MailingList.objects.filter(name = list_name).count(), 1)
    delete_list(list_name)


def test_delete_list():
    delete_list(list_name)
    mlist = find_list(list_name)
    assert not mlist, "Found list: %s, should not." % mlist


def test_remove_all_subscriptions():
    test_add_subscriber()

    remove_all_subscriptions(user_full_address)
    subs = find_subscriptions(user_full_address)
    assert_equal(len(subs), 0)


def test_add_subscriber():
    remove_all_subscriptions(user_full_address)
    sub = add_subscriber(user_full_address, list_name)
    assert sub
    assert_equal(sub.subscriber_address, user_address)
    assert_equal(sub.subscriber_name, user_name)

    subs = find_subscriptions(user_full_address)
    assert_equal(len(subs), 1)


def test_remove_subscriber():
    test_add_subscriber()
    remove_subscriber(user_full_address, list_name)
    subs = find_subscriptions(user_full_address, list_name=list_name)
    assert_equal(len(subs), 0)


def test_post_message():
    for i in range(0,3):
        add_subscriber(user_full_address, list_name)

    sample = MailResponse(To=list_name + "@librelist.com",
                          From=user_full_address,
                          Subject="Test post message.",
                          Body="I am telling you guys you are wrong.")

    sample['Message-Id'] = '12313123123123123'

    msg = MailRequest("fakepeer", sample['from'], sample['to'], str(sample))
    post_message(relay(port=8825), msg, list_name, "librelist.com")


def test_disable_enable_all_subscriptions():
    test_add_subscriber()
    disable_all_subscriptions(user_address)
    assert not find_subscriptions(user_address)

    enable_all_subscriptions(user_address)
    assert find_subscriptions(user_address)

def test_similarily_named_lists():
    test_names = ['test.lists', 'tests.list', 'querylists', 'evil.named',
                 'shouldnot', 'teller.list']
    for name in test_names:
        create_list(name)

    similar = similar_named_lists(list_name)
    assert_equal(len(similar), 2)

    nothing = similar_named_lists("zed.shaw")
    assert not nothing

    similar = similar_named_lists('teler.list')
    assert_equal(len(similar), 1)


def test_craft_response_attachment():
    sample = MailResponse(To=list_name + "@librelist.com",
                          From=user_full_address,
                          Subject="Test message with attachments.",
                          Body="The body as one attachment.")

    sample.attach(filename="tests/model/mailinglist_tests.py",
                  content_type="text/plain",
                  disposition="attachment")

    sample['message-id'] = '123545666'

    im = sample.to_message()
    assert_equal(len([x for x in im.walk()]), 3)
    
    inmsg = MailRequest("fakepeer", None, None, str(sample))
    assert_equal(len(inmsg.all_parts()), 2)

    outmsg = craft_response(inmsg, list_name, list_name +
                                        "@librelist.com")
  
    om = outmsg.to_message()

    assert_equal(len([x for x in om.walk()]),
                 len([x for x in im.walk()]))

    assert 'message-id' in outmsg


def test_craft_response_no_attachment():
    sample = MailResponse(To=list_name + "@librelist.com",
                          From=user_full_address,
                          Subject="Test message with attachments.",
                          Body="The body as one attachment.")

    im = sample.to_message()
    assert_equal(len([x for x in im.walk()]), 1)
    assert_equal(im.get_payload(), sample.Body)
    
    inmsg = MailRequest("fakepeer", None, None, str(sample))
    assert_equal(len(inmsg.all_parts()), 0)
    assert_equal(inmsg.body(), sample.Body)

    outmsg = craft_response(inmsg, list_name, list_name +
                                        "@librelist.com")
  
    om = outmsg.to_message()
    assert_equal(om.get_payload(), sample.Body)

    assert_equal(len([x for x in om.walk()]),
                 len([x for x in im.walk()]))


����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/model/state_storage_tests.py�������������������������������0000644�0000765�0000024�00000002012�11306514042�025544� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from app.model.state_storage import UserStateStorage
from webapp.librelist.models import UserState
from lamson.routing import ROUTE_FIRST_STATE


def setup():
    for state in UserState.objects.all():
        state.delete()

def test_clear():
    ss = UserStateStorage()
    ss.clear()
    assert_equal(len(UserState.objects.all()), 0)


def test_set():
    ss = UserStateStorage()
    # start states should not be stored
    ss.set("app.handlers.admin", "zedshaw@zedshaw.com", "START")
    assert_equal(len(UserState.objects.all()), 0)

    ss.set("app.handlers.admin", "zedshaw@zedshaw.com", "POSTING")
    assert_equal(len(UserState.objects.all()), 1)
    
    ss.clear()

def test_get():
    ss = UserStateStorage()
    ss.clear()
    state = ss.get("app.handlers.admin", "zedshaw@zedshaw.com")
    assert_equal(state, ROUTE_FIRST_STATE)

    ss.set("app.handlers.admin", "zedshaw@zedshaw.com", "POSTING")
    state = ss.get("app.handlers.admin", "zedshaw@zedshaw.com")
    assert_equal(state, "POSTING")
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/templates/�������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022023� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/tests/templates/__init__.py��������������������������������������0000644�0000765�0000024�00000000000�11225417645�024121� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020141� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/__init__.py�����������������������������������������������0000755�0000765�0000024�00000000000�11226011503�022222� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022132� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/__init__.py�������������������������������������0000755�0000765�0000024�00000000000�11226012203�024211� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/admin.py����������������������������������������0000644�0000765�0000024�00000000244�11226243205�023561� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from webapp.librelist.models import *
from django.contrib import admin


for m in [Confirmation, UserState, MailingList, Subscription]:
    admin.site.register(m)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/migrations/�������������������������������������0000755�0000765�0000024�00000000000�11313464574�024306� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/migrations/0001_initial.py����������������������0000644�0000765�0000024�00000011272�11230503443�026737� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
from south.db import db
from django.db import models
from webapp.librelist.models import *

class Migration:
    
    def forwards(self, orm):
        
        # Adding model 'Subscription'
        db.create_table('librelist_subscription', (
            ('subscriber_name', models.CharField(max_length=200)),
            ('enabled', models.BooleanField(default=True)),
            ('created_on', models.DateTimeField(auto_now_add=True)),
            ('subscriber_address', models.EmailField()),
            ('id', models.AutoField(primary_key=True)),
            ('mailing_list', models.ForeignKey(orm.MailingList)),
        ))
        db.send_create_signal('librelist', ['Subscription'])
        
        # Adding model 'UserState'
        db.create_table('librelist_userstate', (
            ('created_on', models.DateTimeField(auto_now_add=True)),
            ('state', models.CharField(max_length=200)),
            ('id', models.AutoField(primary_key=True)),
            ('state_key', models.CharField(max_length=512)),
            ('from_address', models.EmailField()),
        ))
        db.send_create_signal('librelist', ['UserState'])
        
        # Adding model 'Confirmation'
        db.create_table('librelist_confirmation', (
            ('from_address', models.EmailField()),
            ('request_date', models.DateTimeField(auto_now_add=True)),
            ('expected_secret', models.CharField(max_length=50)),
            ('pending_message_id', models.CharField(max_length=200)),
            ('list_name', models.CharField(max_length=200)),
            ('id', models.AutoField(primary_key=True)),
        ))
        db.send_create_signal('librelist', ['Confirmation'])
        
        # Adding model 'MailingList'
        db.create_table('librelist_mailinglist', (
            ('name', models.CharField(max_length=512)),
            ('archive_url', models.CharField(max_length=512)),
            ('similarity_pri', models.CharField(max_length=50)),
            ('archive_queue', models.CharField(max_length=512)),
            ('similarity_sec', models.CharField(max_length=50, null=True)),
            ('created_on', models.DateTimeField(auto_now_add=True)),
            ('id', models.AutoField(primary_key=True)),
        ))
        db.send_create_signal('librelist', ['MailingList'])
        
    
    
    def backwards(self, orm):
        
        # Deleting model 'Subscription'
        db.delete_table('librelist_subscription')
        
        # Deleting model 'UserState'
        db.delete_table('librelist_userstate')
        
        # Deleting model 'Confirmation'
        db.delete_table('librelist_confirmation')
        
        # Deleting model 'MailingList'
        db.delete_table('librelist_mailinglist')
        
    
    
    models = {
        'librelist.subscription': {
            'created_on': ('models.DateTimeField', [], {'auto_now_add': 'True'}),
            'enabled': ('models.BooleanField', [], {'default': 'True'}),
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'mailing_list': ('models.ForeignKey', ['MailingList'], {}),
            'subscriber_address': ('models.EmailField', [], {}),
            'subscriber_name': ('models.CharField', [], {'max_length': '200'})
        },
        'librelist.userstate': {
            'created_on': ('models.DateTimeField', [], {'auto_now_add': 'True'}),
            'from_address': ('models.EmailField', [], {}),
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'state': ('models.CharField', [], {'max_length': '200'}),
            'state_key': ('models.CharField', [], {'max_length': '512'})
        },
        'librelist.confirmation': {
            'expected_secret': ('models.CharField', [], {'max_length': '50'}),
            'from_address': ('models.EmailField', [], {}),
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'list_name': ('models.CharField', [], {'max_length': '200'}),
            'pending_message_id': ('models.CharField', [], {'max_length': '200'}),
            'request_date': ('models.DateTimeField', [], {'auto_now_add': 'True'})
        },
        'librelist.mailinglist': {
            'archive_queue': ('models.CharField', [], {'max_length': '512'}),
            'archive_url': ('models.CharField', [], {'max_length': '512'}),
            'created_on': ('models.DateTimeField', [], {'auto_now_add': 'True'}),
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'name': ('models.CharField', [], {'max_length': '512'}),
            'similarity_pri': ('models.CharField', [], {'max_length': '50'}),
            'similarity_sec': ('models.CharField', [], {'max_length': '50', 'null': 'True'})
        }
    }
    
    complete_apps = ['librelist']
��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/migrations/__init__.py��������������������������0000644�0000765�0000024�00000000000�11230503443�026370� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/models.py���������������������������������������0000755�0000765�0000024�00000003275�11230232201�023753� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from django.db import models
from datetime import datetime
from email.utils import formataddr

# Create your models here.

class Confirmation(models.Model):
    from_address = models.EmailField()
    request_date = models.DateTimeField(auto_now_add=True)
    expected_secret = models.CharField(max_length=50)
    pending_message_id = models.CharField(max_length=200)
    list_name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.from_address

class UserState(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    state_key = models.CharField(max_length=512)
    from_address = models.EmailField()
    state = models.CharField(max_length=200)

    def __unicode__(self):
        return "%s:%s (%s)" % (self.state_key, self.from_address, self.state)

class MailingList(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    archive_url = models.CharField(max_length=512)
    archive_queue = models.CharField(max_length=512)
    name = models.CharField(max_length=512)
    similarity_pri = models.CharField(max_length=50)
    similarity_sec = models.CharField(max_length=50, null=True)

    def __unicode__(self):
        return self.name


class Subscription(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    subscriber_address = models.EmailField()
    subscriber_name = models.CharField(max_length=200)
    mailing_list = models.ForeignKey(MailingList)
    enabled = models.BooleanField(default=True)

    def __unicode__(self):
        return '"%s" <%s>' % (self.subscriber_name, self.subscriber_address)

    def subscriber_full_address(self):
        return formataddr((self.subscriber_name, self.subscriber_address))
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/urls.py�����������������������������������������0000644�0000765�0000024�00000000106�11226014151�023447� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from django.conf.urls.defaults import *

urlpatterns = patterns('',)

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/librelist/views.py����������������������������������������0000755�0000765�0000024�00000000032�11226012203�023614� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Create your views here.
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/manage.py�������������������������������������������������0000755�0000765�0000024�00000001042�11226011503�021722� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python
from django.core.management import execute_manager
try:
    import settings # Assumed to be in the same directory.
except ImportError:
    import sys
    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
    sys.exit(1)

if __name__ == "__main__":
    execute_manager(settings)
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/settings.py�����������������������������������������������0000755�0000765�0000024�00000005571�11230503143�022346� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Django settings for webapp project.
import os


DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
    ('Zed A. Shaw', 'zedshaw@librelist.com'),
)

MANAGERS = ADMINS

DATABASE_ENGINE = 'sqlite3'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = os.path.dirname(__file__) + '/../run/data.sqlite3'             # Or path to database file if using sqlite3.
DATABASE_USER = ''             # Not used with sqlite3.
DATABASE_PASSWORD = ''         # Not used with sqlite3.
DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''

# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'

# Make this unique, and don't share it with anybody.
SECRET_KEY = '5#x-^z6gdg$*x&^1f0qjn=jj^*dopzn5-w52qeo13#11#xz&vw'

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
#     'django.template.loaders.eggs.load_template_source',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

ROOT_URLCONF = 'webapp.urls'

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'django.contrib.admindocs',
    'webapp.librelist',
    'south',
)
���������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/librelist/webapp/urls.py���������������������������������������������������0000755�0000765�0000024�00000001017�11226015021�021460� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    (r'^/', include('webapp.librelist.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
    # to INSTALLED_APPS to enable admin documentation:
    (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    (r'^admin/(.*)', admin.site.root),
)
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/�����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020147� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020727� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/__init__.py��������������������������������������������0000644�0000765�0000024�00000000000�11236177155�023026� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/handlers/����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022527� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/handlers/__init__.py�����������������������������������0000644�0000765�0000024�00000000000�11236177155�024626� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/handlers/anonymizer.py���������������������������������0000644�0000765�0000024�00000004662�11242130642�025267� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config.settings import relay, BOUNCES, SPAM, CONFIRM
from lamson.routing import route, Router, route_like
from lamson.bounce import bounce_to
from lamson.spam import spam_filter
from lamson import view, queue, confirm
from app.model import filter, addressing
import logging


@route(".+")
def IGNORE_BOUNCE(message):
    bounces = queue.Queue(BOUNCES)
    bounces.push(message)
    return START

@route(".+")
def SPAMMING(message):
    return SPAMMING

@route("start@(host)")
@route("(user_id)@(host)")
@bounce_to(soft=IGNORE_BOUNCE, hard=IGNORE_BOUNCE)
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, user_id=None, host=None):
    if user_id:
        market_anon = addressing.mapping(message['from'], 'marketroid', host)

        reply = filter.cleanse_incoming(message, user_id, host, market_anon)
        relay.deliver(reply)

        return DEMARKETING
    else:
        CONFIRM.send(relay, "start", message, "mail/start_confirm.msg", locals())
        return CONFIRMING


@route("start-confirm-(id_number)@(host)")
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('start', message['from'], id_number)

    if original:
        user_anon = addressing.mapping(message['from'], 'user', host)

        welcome = view.respond(locals(), "mail/welcome.msg", 
                           From=user_anon,
                           To=message['from'],
                           Subject="Welcome to MyInboxIsNotA.TV")
        relay.deliver(welcome)

        return PROTECTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING


@route("(user_id)@(host)")
def DEMARKETING(message, user_id=None, host=None):
    reply = filter.cleanse_incoming(message, user_id, host)
    relay.deliver(reply)
    return DEMARKETING


@route("(marketroid_id)@(host)")
@route("(user_id)@(host)")
def PROTECTING(message, marketroid_id=None, host=None, user_id=None):
    if user_id:
        logging.warning("Attempted user->user email from %r", message['from'])
        forbid =  view.respond(locals(), "mail/forbid.msg",
                               From="noreply@%(host)s",
                               To=message['from'],
                               Subject="You cannot email another user or yourself.")
        relay.deliver(forbid)
    else:
        reply = filter.route_reply(message, marketroid_id, host)
        relay.deliver(reply)

    return PROTECTING

������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/model/�������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022027� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/model/__init__.py��������������������������������������0000644�0000765�0000024�00000000000�11236177155�024126� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/model/addressing.py������������������������������������0000644�0000765�0000024�00000001736�11237413244�024524� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from pytyrant import PyTyrant
import uuid
import zlib
from email.utils import parseaddr


def lookup(address, host=None):
    name, addr = parseaddr(address)
    table = PyTyrant.open()
    user = table[addr]
    table.close()

    if host:
        return user + '@' + host
    else:
        return user

def store(address, maps_to):
    name, addr = parseaddr(address)
    table = PyTyrant.open()
    table[addr] = maps_to
    table.close()

def delete(address):
    name, addr = parseaddr(address)
    table = PyTyrant.open()
    del table[addr]
    table.close()

def random_id():
    return "%x" % abs(zlib.adler32(uuid.uuid4().hex))


def mapping(real_address, anon_type, host):
    assert anon_type in ['user', 'marketroid']
    
    anon_id = "%s-%s" % (anon_type, random_id())

    store(anon_id, real_address)
    store(real_address, anon_id)

    return anon_id + '@' + host


def real(user_id):
    return lookup(user_id)


def anon(real, host):
    return lookup(real, host)



����������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/model/filter.py����������������������������������������0000644�0000765�0000024�00000003030�11237226434�023656� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import mail
from app.model import addressing


def craft_response(message, From, To, contact_addr=None):
    response = mail.MailResponse(To=To,
                            From=From,
                            Subject=message['subject'])

    msg_id = message['message-id']

    if contact_addr:
        response.update({
            "Sender": contact_addr, 
            "Reply-To": contact_addr,
            "Return-Path": contact_addr, 
            "Precedence": "list",
        })

    if 'date' in message:
        response['Date'] = message['date']

    if 'references' in message:
        response['References'] = message['References']
    elif msg_id:
        response['References'] = msg_id

    if msg_id:
        response['message-id'] = msg_id

        if 'in-reply-to' not in message:
            response["In-Reply-To"] = message['Message-Id']

    if message.all_parts():
        response.attach_all_parts(message)
    else:
        response.Body = message.body()

    return response


def cleanse_incoming(message, user_id, host, marketroid_rand=None):
    user_real = addressing.real(user_id)

    if not marketroid_rand:
        marketroid_rand = addressing.anon(message['from'], host)

    reply = craft_response(message, message['from'], user_real, marketroid_rand)

    return reply


def route_reply(message, marketroid_id, host):
    marketroid_real = addressing.real(marketroid_id)
    user_anon = addressing.anon(message['from'], host)

    reply = craft_response(message, user_anon, marketroid_real)

    return reply


��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/model/html.py������������������������������������������0000644�0000765�0000024�00000004072�11236224053�023336� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lxml import etree, sax
from xml.sax.handler import ContentHandler, EntityResolver

# stolen from http://code.activestate.com/recipes/148061/
def wrap(text, width):
    return reduce(lambda line, word, width=width: '%s%s%s' %
                  (line,
                   ' \n'[(len(line)-line.rfind('\n')-1
                         + len(word.split('\n',1)[0]
                              ) >= width)],
                   word),
                  text.split(' ')
                 )


class TextOnlyContentHandler(ContentHandler):
    def __init__(self):
        self.text = []
        self.stack = []
        self.links = []

    def startElementNS(self, name, qname, attributes):
        if qname in ["h1", "h2", "h3"]:
            self.stack.append((qname, ""))
        elif qname == "a":
            href = attributes.getValueByQName("href")
            self.stack.append((qname, href))
        elif qname == "p":
            if self.stack:
                self.text.append(self.stack.pop()[1])
            self.text.append("\n\n")

    def characters(self, text_data):
        if text_data.strip():
            data = " ".join([l.strip() for l in text_data.split("\n")]).strip()

            if self.stack:
                qname, text = self.stack.pop()
                if qname in ["h1","h2","h3"]:
                    self.text.append("\n\n" + data + "\n" + "=" * len(data) + "\n")
                elif qname == "a":
                    if text not in self.links:
                        self.links.append(text)

                    index = self.links.index(text)
                    self.text.append(data + "[%d]" % len(self.links))
                else:
                    self.text.append(text + " " + data)
            else:
                self.text.append(data)


def strip_html(doc):
    tree = etree.fromstring(doc)
    handler = TextOnlyContentHandler()
    sax.saxify(tree, handler)
    links_list = ""
    for i, link in enumerate(handler.links):
        links_list += "\n[%d] %s" % (i+1, link)

    text = " ".join(handler.text)
    return wrap(text, 72) + "\n\n----" + links_list


����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/templates/���������������������������������������������0000755�0000765�0000024�00000000000�11313464573�022724� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/templates/mail/����������������������������������������0000755�0000765�0000024�00000000000�11313464574�023647� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/templates/mail/forbid.msg������������������������������0000644�0000765�0000024�00000001532�11237377733�025632� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

This is a receive-only service for filtering email from non-registered
users.  That means we can't let you email another user from your
account, and in fact if you keep trying to do it we'll change you to a
marketroid classification instead.

If we let you do this, then marketroids could just subscribe for the
service themselves and then continue spamming people through their own
MyInboxIsNotA.TV account.


YOUR MESSAGE IS DROPPED

The email you just sent is dropped.  If we see many of these attempts
from your account we'll reclassify you as a marketroid and publish your
address on the wall of shame.

Don't worry, we'll warn you before we do this, but consider this the
first step.


TESTING YOUR SHIELD ADDRESS

If you are trying to test your address, then go use another email
address from a free webmail service.


Thanks,

MyInboxIsNotA.TV

����������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/templates/mail/start_confirm.msg�����������������������0000644�0000765�0000024�00000001741�11237232733�027227� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

So you're interested in MyInboxIsNotA.TV are you?  Well, before we can
let you use our service we have to get you to confirm that you really
want to use it.  If we don't then someone could subscribe you without
your consent.


CONFIRMING

To confirm that you want to start using our server, simply reply to this
message.  Put anything you want in the body and subject.  When we
receive your reply we'll sign you up and send you instructions on using
MyInboxIsNotA.TV.


LEGAL STUFF

By replying to this email you agree that we can terminate your service
at any time, that we do not guarantee security or confidentiality, and
that we will give your email address and history to any law enforcement
agency that asks politely.  Replying means you understand that we will
gleefully rat your ass out if you're doing anything moderately illegal.

However, we do promise to *never* give your real address to any marketroid
unless they sue us to find out who you are.


Thanks,

MyInboxIsNotA.TV
�������������������������������lamson-1.0pre11/examples/myinboxisnotatv/app/templates/mail/welcome.msg�����������������������������0000644�0000765�0000024�00000002705�11237377760�026023� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Welcome,

You are now allowed to use MyInboxIsNotA.TV to communicate with annoying
people who might send you spam, HTML email, or who you just don't want
to give your real address to.

The goal of our service is to make it clear that people who try to
market to you should not assume that sending you an email is like making
you watch a TV commercial.  If they don't get this, then they shouldn't
be using email for customer development.


YOUR SHIELD ADDRESS

When you want to signup for a service, simply give out this address:

{{ user_anon }}

Remember it!  Write it down!  This is important!


HOW IT WORKS

When you give your SHIELD ADDRESS (you wrote it down right?) to a
service, you are not giving your real address to them.  Now when they
try to send you an email, it comes to myinboxisnota.tv first.

We then rewrite it so that it gets sent to you, and give you a special
"marketroid" address to talk with them over.  You get to see the
original sender, but replying to this email will go back to
myinboxisnota.tv.

When we receive a reply to this marketroid address, we rewrite it so
that any semblance of your identity is removed from the headers and
replaced with {{ user_anon }}.


CAUTIONS

If you want to remain anonymous, you should probably make sure you do
the following:

* Remove anything identifying you from your signature.
* Try not to use your real name with the service.
* Don't use your real name in any correspondence.


Thanks,

MyInboxIsNotA.TV

�����������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021414� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/__init__.py�����������������������������������������0000644�0000765�0000024�00000000000�11236177155�023513� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/boot.py���������������������������������������������0000644�0000765�0000024�00000001665�11237410102�022720� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson import view
import logging
import logging.config
import jinja2

logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.LOG_EXCEPTIONS=True
Router.UNDELIVERABLE_QUEUE=queue.Queue(settings.UNDELIVERABLES)

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

���������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/logging.conf����������������������������������������0000644�0000765�0000024�00000001064�11236177155�023712� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=fileHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[logger_routing]
level=DEBUG
handlers=fileHandler
qualname=routing
propagate=0

[handler_fileHandler]
# this works using FileHandler
class=FileHandler
# If you have Python2.6 you can use this and it will work when you use logrotate
#class=WatchedFileHandler
level=DEBUG
formatter=defaultFormatter
args=("logs/lamson.log",)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/settings.py�����������������������������������������0000644�0000765�0000024�00000001427�11242130703�023613� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file contains python variables that configure Lamson for email processing.
import logging
import shelve
from lamson import confirm

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

handlers = ['app.handlers.anonymizer']

router_defaults = {
    'host': 'myinboxisnota.tv',
    'user_id': 'user-[a-z0-9]+',
    'marketroid_id': 'marketroid-[a-z0-9]+',
    'id_number': '[a-z0-9]+',
}

template_config = {'dir': 'app', 'module': 'templates'}

SPAM = {'db': 'run/spamdb', 'rc': 'run/spamrc', 'queue': 'run/spam'}

BOUNCES = 'run/bounces'
UNDELIVERABLES = 'run/undeliverables'
CONFIRM_STORAGE=confirm.ConfirmationStorage(db=shelve.open("run/confirmationsdb"))

CONFIRM = confirm.ConfirmationEngine('run/pending', CONFIRM_STORAGE)
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/test_logging.conf�����������������������������������0000644�0000765�0000024�00000001042�11236177155�024745� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=stdoutHandler,stderrHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=stdoutHandler

[logger_routing]
level=DEBUG
handlers=stderrHandler
qualname=routing
propagate=0

[handler_stdoutHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)

[handler_stderrHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stderr,)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/config/testing.py������������������������������������������0000644�0000765�0000024�00000002103�11236177155�023437� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson import view
from lamson.routing import Router
from lamson.server import Relay
import jinja2
import logging
import logging.config
import os

logging.config.fileConfig("config/test_logging.conf")

# the relay host to actually send the final message to (set debug=1 to see what
# the relay is saying to the log server).
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=0)


settings.receiver = None

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.LOG_EXCEPTIONS=False

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

# if you have pyenchant and enchant installed then the template tests will do
# spell checking for you, but you need to tell pyenchant where to find itself
# if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
#     os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021443� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/backup����������������������������������������������0000644�0000765�0000024�00000000132�11236177362�022627� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup

backup $TARGET $BACKUP

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/env/������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022233� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/env/testing�����������������������������������������0000644�0000765�0000024�00000001315�11236177362�023633� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# An deploy/env file contains the settings and special functions
# for that environment.  


LOG4SH_CONFIGURATION=deploy/log4sh.properties
SOURCE=$HOME/projects/lamson/examples/librelist
TARGET=$HOME/deploy/testing
BACKUP=$HOME/deploy/backup/testing
FAILS=$HOME/deploy/fails/testing


function stop {
    logger_info "Stopping testing deployment."
    pushd $1
    assert_in $1

    lamson stop -ALL run
    popd
}


function start {
    logger_info "Starting testing deployment."
    pushd $1
    assert_in $1

    lamson start
    popd
}


function setup {
    assert_in $SOURCE

    mkdir -p $SOURCE
    mkdir -p $TARGET
    mkdir -p $BACKUP
    mkdir -p $FAILS

    rm -rf run/*
    rm -rf app/data/archive/*
}


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/forward���������������������������������������������0000644�0000765�0000024�00000000250�11236177363�023030� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup

stop $TARGET

backup $TARGET $BACKUP

deploy_code $SOURCE $TARGET/..

migrate $TARGET

start $TARGET

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/lib/������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022211� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/lib/log4sh������������������������������������������0000644�0000765�0000024�00000335345�11236177363�023352� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# $Id: log4sh 574 2007-06-02 20:09:15Z sfsetse $
# vim:syntax=sh:sts=2
# vim:foldmethod=marker:foldmarker=/**,*/
#
#/**
# <?xml version="1.0" encoding="UTF-8"?>
# <s:shelldoc xmlns:s="http://www.forestent.com/2005/XSL/ShellDoc">
# <s:header>
# log4sh 1.4.2
#
# http://log4sh.sourceforge.net/
#
# written by Kate Ward &lt;kate.ward@forestent.com>
# released under the LGPL
#
# this module implements something like the log4j module from the Apache group
#
# notes:
# *) the default appender is a ConsoleAppender named stdout with a level
#    of ERROR and layout of SimpleLayout
# *) the appender levels are as follows (decreasing order of output):
#    TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
# </s:header>
#*/

# shell flags for log4sh:
# u - treat unset variables as an error when performing parameter expansion
__LOG4SH_SHELL_FLAGS='u'

# save the current set of shell flags, and then set some for log4sh
__log4sh_oldShellFlags=$-
for _log4sh_shellFlag in `echo "${__LOG4SH_SHELL_FLAGS}" |sed 's/\(.\)/\1 /g'`
do
  set -${_log4sh_shellFlag}
done

#
# constants
#
__LOG4SH_VERSION='1.4.2'

__LOG4SH_TRUE=0
__LOG4SH_FALSE=1
__LOG4SH_ERROR=2
__LOG4SH_NULL='~'

__LOG4SH_APPENDER_FUNC_PREFIX='_log4sh_app_'
__LOG4SH_APPENDER_INCLUDE_EXT='.inc'

__LOG4SH_TYPE_CONSOLE='ConsoleAppender'
__LOG4SH_TYPE_DAILY_ROLLING_FILE='DailyRollingFileAppender'
__LOG4SH_TYPE_FILE='FileAppender'
__LOG4SH_TYPE_ROLLING_FILE='RollingFileAppender'
__LOG4SH_TYPE_ROLLING_FILE_MAX_BACKUP_INDEX=1
__LOG4SH_TYPE_ROLLING_FILE_MAX_FILE_SIZE=10485760
__LOG4SH_TYPE_SMTP='SMTPAppender'
__LOG4SH_TYPE_SYSLOG='SyslogAppender'
__LOG4SH_TYPE_SYSLOG_FACILITY_NAMES=' kern user mail daemon auth security syslog lpr news uucp cron authpriv ftp local0 local1 local2 local3 local4 local5 local6 local7 '
__LOG4SH_TYPE_SYSLOG_FACILITY='user'

__LOG4SH_LAYOUT_HTML='HTMLLayout'
__LOG4SH_LAYOUT_SIMPLE='SimpleLayout'
__LOG4SH_LAYOUT_PATTERN='PatternLayout'

__LOG4SH_LEVEL_TRACE=0
__LOG4SH_LEVEL_TRACE_STR='TRACE'
__LOG4SH_LEVEL_DEBUG=1
__LOG4SH_LEVEL_DEBUG_STR='DEBUG'
__LOG4SH_LEVEL_INFO=2
__LOG4SH_LEVEL_INFO_STR='INFO'
__LOG4SH_LEVEL_WARN=3
__LOG4SH_LEVEL_WARN_STR='WARN'
__LOG4SH_LEVEL_ERROR=4
__LOG4SH_LEVEL_ERROR_STR='ERROR'
__LOG4SH_LEVEL_FATAL=5
__LOG4SH_LEVEL_FATAL_STR='FATAL'
__LOG4SH_LEVEL_OFF=6
__LOG4SH_LEVEL_OFF_STR='OFF'
__LOG4SH_LEVEL_CLOSED=255
__LOG4SH_LEVEL_CLOSED_STR='CLOSED'

__LOG4SH_PATTERN_DEFAULT='%d %p - %m%n'
__LOG4SH_THREAD_DEFAULT='main'

__LOG4SH_CONFIGURATION="${LOG4SH_CONFIGURATION:-log4sh.properties}"
__LOG4SH_CONFIG_PREFIX="${LOG4SH_CONFIG_PREFIX:-log4sh}"
__LOG4SH_CONFIG_LOG4J_CP='org.apache.log4j'

# the following IFS is *supposed* to be on two lines!!
__LOG4SH_IFS_ARRAY="
"
__LOG4SH_IFS_DEFAULT=' '

__LOG4SH_SECONDS=`eval "expr \`date '+%H \* 3600 + %M \* 60 + %S'\`"`

# configure log4sh debugging. set the LOG4SH_INFO environment variable to any
# non-empty value to enable info output, LOG4SH_DEBUG enable debug output, or
# LOG4SH_TRACE to enable trace output. log4sh ERROR and above messages are
# always printed. to send the debug output to a file, set the LOG4SH_DEBUG_FILE
# with the filename you want debug output to be written to.
__LOG4SH_TRACE=${LOG4SH_TRACE:+'_log4sh_trace '}
__LOG4SH_TRACE=${__LOG4SH_TRACE:-':'}
[ -n "${LOG4SH_TRACE:-}" ] && LOG4SH_DEBUG=1
__LOG4SH_DEBUG=${LOG4SH_DEBUG:+'_log4sh_debug '}
__LOG4SH_DEBUG=${__LOG4SH_DEBUG:-':'}
[ -n "${LOG4SH_DEBUG:-}" ] && LOG4SH_INFO=1
__LOG4SH_INFO=${LOG4SH_INFO:+'_log4sh_info '}
__LOG4SH_INFO=${__LOG4SH_INFO:-':'}

# set the constants to readonly
for _log4sh_const in `set |grep "^__LOG4SH_" |cut -d= -f1`; do
  readonly ${_log4sh_const}
done
unset _log4sh_const

#
# internal variables
#

__log4sh_filename=`basename $0`
__log4sh_tmpDir=''
__log4sh_trapsFile=''

__log4sh_alternative_mail='mail'

__log4sh_threadName=${__LOG4SH_THREAD_DEFAULT}
__log4sh_threadStack=${__LOG4SH_THREAD_DEFAULT}

__log4sh_seconds=0
__log4sh_secondsLast=0
__log4sh_secondsWrap=0

# workarounds for various commands
__log4sh_wa_strictBehavior=${__LOG4SH_FALSE}
(
  # determine if the set builtin needs to be evaluated. if the string is parsed
  # into two separate strings (common in ksh), then set needs to be evaled.
  str='x{1,2}'
  set -- ${str}
  test ! "$1" = 'x1' -a ! "${2:-}" = 'x2'
)
__log4sh_wa_setNeedsEval=$?


#=============================================================================
# Log4sh
#

#-----------------------------------------------------------------------------
# internal debugging
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_log</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_log()
{
  _ll__level=$1
  shift
  if [ -z "${LOG4SH_DEBUG_FILE:-}" ]; then
    echo "log4sh:${_ll__level} $@" >&2
  else
    echo "${_ll__level} $@" >>${LOG4SH_DEBUG_FILE}
  fi
  unset _ll__level
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_trace</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_trace()
{
  _log4sh_log "${__LOG4SH_LEVEL_TRACE_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
 }

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_debug</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_debug()
{
  _log4sh_log "${__LOG4SH_LEVEL_DEBUG_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_info</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_info()
{
  _log4sh_log "${__LOG4SH_LEVEL_INFO_STR}" "${BASH_LINENO:+(${BASH_LINENO}) }- $@";
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_warn</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_warn()
{
  echo "log4sh:${__LOG4SH_LEVEL_WARN_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_WARN_STR}" "$@"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_error</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_error()
{
  echo "log4sh:${__LOG4SH_LEVEL_ERROR_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_ERROR_STR}" "$@"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_fatal</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is an internal debugging function. It should not be called.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_log</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_fatal()
{
  echo "log4sh:${__LOG4SH_LEVEL_FATAL_STR} $@" >&2
  [ -n "${LOG4SH_DEBUG_FILE:-}" ] \
    && _log4sh_log "${__LOG4SH_LEVEL_FATAL_STR}" "$@"
}

#-----------------------------------------------------------------------------
# miscellaneous
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_mktempDir</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Creates a secure temporary directory within which temporary files can be
#     created. Honors the <code>TMPDIR</code> environment variable if it is
#     set.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>tmpDir=`_log4sh_mktempDir`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_mktempDir()
{
  _lmd_tmpPrefix='log4sh'

  # try the standard mktemp function
  ( exec mktemp -dqt ${_lmd_tmpPrefix}.XXXXXX 2>/dev/null ) && return

  # the standard mktemp didn't work. doing our own.
  if [ -n "${RANDOM:-}" ]; then
    # $RANDOM works
    _lmd_random=${RANDOM}${RANDOM}${RANDOM}$$
  elif [ -r '/dev/urandom' ]; then
    _lmd_random=`od -vAn -N4 -tu4 </dev/urandom |sed 's/^[^0-9]*//'`
  else
    # $RANDOM doesn't work
    _lmd_date=`date '+%Y%m%d%H%M%S'`
    _lmd_random=`expr ${_lmd_date} / $$`
    unset _lmd_date
  fi

  _lmd_tmpDir="${TMPDIR:-/tmp}/${_lmd_tmpPrefix}.${_lmd_random}"
  ( umask 077 && mkdir "${_lmd_tmpDir}" ) || {
    _log4sh_fatal 'could not create temporary directory! exiting'
    exit 1
  }

  ${__LOG4SH_DEBUG} "created temporary directory (${_lmd_tmpDir})"
  echo "${_lmd_tmpDir}"
  unset _lmd_random _lmd_tmpDir _lmd_tmpPrefix
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_updateSeconds</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the <code>__log4sh_seconds</code> variable to the number of seconds
#     elapsed since the start of the script.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_updateSeconds`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_updateSeconds()
{
  if [ -n "${SECONDS:-}" ]; then
    __log4sh_seconds=${SECONDS}
  else
    _lgs__date=`date '+%H \* 3600 + %M \* 60 + %S'`
    _lgs__seconds=`eval "expr ${_lgs__date} + ${__log4sh_secondsWrap} \* 86400"`
    if [ ${_lgs__seconds} -lt ${__log4sh_secondsLast} ]; then
      __log4sh_secondsWrap=`expr ${__log4sh_secondsWrap} + 1`
      _lgs__seconds=`expr ${_lgs_seconds} + 86400`
    fi
    __log4sh_seconds=`expr ${_lgs__seconds} - ${__LOG4SH_SECONDS}`
    __log4sh_secondsLast=${__log4sh_seconds}
    unset _lgs__date _lgs__seconds
  fi
}

#/**
# <s:function group="Log4sh" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_enableStrictBehavior</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Enables strict log4j behavior.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_enableStrictBehavior</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_enableStrictBehavior()
{
  __log4sh_wa_strictBehavior=${__LOG4SH_TRUE}
}

#/**
# <s:function group="Log4sh" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_setAlternative</function></funcdef>
#       <paramdef>string <parameter>command</parameter></paramdef>
#       <paramdef>string <parameter>path</parameter></paramdef>
#       <paramdef>boolean <parameter>useRuntimePath</parameter> (optional)</paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Specifies an alternative path for a command.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_setAlternative nc /bin/nc</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_setAlternative()
{
  if [ $# -lt 2 ]; then
    _log4sh_error 'log4sh_setAlternative(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  lsa_cmdName=$1
  lsa_cmdPath=$2
  lsa_useRuntimePath=${3:-}
  __log4sh_return=${__LOG4SH_TRUE}

  # check that the alternative command exists and is executable
  if [ \
    ! -x "${lsa_cmdPath}" \
    -a ${lsa_useRuntimePath:-${__LOG4SH_FALSE}} -eq ${__LOG4SH_FALSE} \
  ]; then
    # the alternative command is not executable
    _log4sh_error "log4sh_setAlternative(): ${lsa_cmdName}: command not found"
    __log4sh_return=${__LOG4SH_ERROR}
  fi

  # check for valid alternative
  if [ ${__log4sh_return} -eq ${__LOG4SH_TRUE} ]; then
    case ${lsa_cmdName} in
      mail) ;;
      nc)
        lsa_cmdVers=`${lsa_cmdPath} --version 2>&1 |head -1`
        if echo "${lsa_cmdVers}" |grep '^netcat' >/dev/null; then
          # GNU Netcat
          __log4sh_alternative_nc_opts='-c'
        else
          # older netcat (v1.10)
          if nc -q 0 2>&1 |grep '^no destination$' >/dev/null 2>&1; then
            # supports -q option
            __log4sh_alternative_nc_opts='-q 0'
          else
            # doesn't support the -q option
            __log4sh_alternative_nc_opts=''
          fi
        fi
        unset lsa_cmdVers
        ;;
      *)
        # the alternative is not valid
        _log4sh_error "unrecognized command alternative '${lsa_cmdName}'"
        __log4sh_return=${__LOG4SH_FALSE}
        ;;
    esac
  fi

  # set the alternative
  if [ ${__log4sh_return} -eq ${__LOG4SH_TRUE} ]; then
    eval __log4sh_alternative_${lsa_cmdName}="\${lsa_cmdPath}"
    ${__LOG4SH_DEBUG} "alternative '${lsa_cmdName}' command set to '${lsa_cmdPath}'"
  fi

  unset lsa_cmdName lsa_cmdPath
  return ${__log4sh_return}
}

#-----------------------------------------------------------------------------
# array handling
#
# note: arrays are '1' based
#

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_findArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Find the position of element in an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>
#       pos=`_log4sh_findArrayElement "$array" $element`
#     </funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_findArrayElement()
{
  __pos=`echo "$1" |awk '$0==e{print NR}' e="$2"`
  [ -n "${__pos}" ] && echo "${__pos}" || echo 0
  unset __pos
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>integer <parameter>position</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Retrieve the element at the given position from an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>element=`_log4sh_getArrayElement "$array" $position`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getArrayElement()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  _lgae_array=$1
  _lgae_index=$2
  ${__LOG4SH_TRACE} "_lgae_array='${_lgae_array}' _lgae_index='${_lgae_index}'"

  _lgae_oldIFS=${IFS} IFS=${__LOG4SH_IFS_ARRAY}
  if [ ${__log4sh_wa_setNeedsEval} -eq 0 ]; then
    set -- junk ${_lgae_array}
  else
    eval "set -- junk \"${_lgae_array}\""
    _lgae_arraySize=$#

    if [ ${_lgae_arraySize} -le ${__log4shAppenderCount} ]; then
      # the evaled set *didn't* work; failing back to original set command and
      # disabling the work around. (pdksh)
      __log4sh_wa_setNeedsEval=${__LOG4SH_FALSE}
      set -- junk ${_lgae_array}
    fi
  fi
  IFS=${_lgae_oldIFS}

  shift ${_lgae_index}
  ${__LOG4SH_TRACE} "1='${1:-}' 2='${2:-}' 3='${3:-}' ..."
  echo "$1"

  unset _lgae_array _lgae_arraySize _lgae_index _lgae_oldIFS
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry align="left">
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getArrayLength</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the length of an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>length=`_log4sh_getArrayLength "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getArrayLength()
{
  _oldIFS=${IFS} IFS=${__LOG4SH_IFS_ARRAY}
  set -- $1
  IFS=${_oldIFS} unset _oldIFS
  echo $#
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string[]</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_setArrayElement</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>integer <parameter>position</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Place an element at a given location in an array</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_setArrayElement "$array" $position $element`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_setArrayElement()
{
  echo "$1" |awk '{if(NR==r){print e}else{print $0}}' r=$2 e="$3"
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_peekStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Return the topmost element on a stack without removing the
#   element.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>element=`_log4sh_peekStack "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_peekStack()
{
  echo "$@" |awk '{line=$0}END{print line}'
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string[]</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_popStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Remove the top-most element from a stack. This command takes a
#   normal log4sh string array as input, but treats it as though it were a
#   stack.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_popStack "$array"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_popStack()
{
  _array=$1
  _length=`_log4sh_getArrayLength "${_array}"`
  echo "${_array}" |awk '{if(NR<r){print $0}}' r=${_length}
  unset _array _length
}

#/**
# <s:function group="Log4sh" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_pushStack</function></funcdef>
#       <paramdef>string[] <parameter>array</parameter></paramdef>
#       <paramdef>string <parameter>element</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Add a new element to the top of a stack. This command takes a normal
#   log4sh string array as input, but treats it as though it were a
#   stack.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newArray=`_log4sh_pushStack "$array" $element`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_pushStack()
{
  echo "${1:+$1${__LOG4SH_IFS_ARRAY}}$2"
}

#=============================================================================
# Appender
#

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_activateOptions</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Activate an appender's configuration. This should be called after
#     reconfiguring an appender via code. It needs only to be called once
#     before any logging statements are called. This calling of this function
#     will be required in log4sh 1.4.x.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_activateAppender myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_activateOptions()
{
  _aao_appender=$1
  ${__LOG4SH_APPENDER_FUNC_PREFIX}${_aao_appender}_activateOptions
  unset _aao_appender
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_close</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Disable any further logging via an appender. Once closed, the
#   appender can be reopened by setting it to any logging Level (e.g.
#   INFO).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_close myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_close()
{
  appender_setLevel $1 ${__LOG4SH_LEVEL_CLOSED_STR}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_exists</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Checks for the existance of a named appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>exists=`appender_exists myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_exists()
{
  _ae_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  [ "${_ae_index}" -gt 0 ] \
    && _ae_return=${__LOG4SH_TRUE} \
    || _ae_return=${__LOG4SH_FALSE}
  unset _ae_index
  return ${_ae_return}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getLayout</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Layout of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getLayout myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getLayout()
{
  _agl_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  _log4sh_getArrayElement "${__log4shAppenderLayouts}" ${_agl_index}
  unset _agl_index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setLayout</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>layout</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Layout of an Appender (e.g. PatternLayout)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setLayout myAppender PatternLayout</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setLayout()
{
  _asl_appender=$1
  _asl_layout=$2

  case ${_asl_layout} in
    ${__LOG4SH_LAYOUT_HTML}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_HTML})
      _asl_layout=${__LOG4SH_LAYOUT_HTML}
      ;;

    ${__LOG4SH_LAYOUT_SIMPLE}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_SIMPLE})
      _asl_layout=${__LOG4SH_LAYOUT_SIMPLE}
      ;;

    ${__LOG4SH_LAYOUT_PATTERN}|\
    ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_LAYOUT_PATTERN})
      _asl_layout=${__LOG4SH_LAYOUT_PATTERN}
      ;;

    *)
      _log4sh_error "unknown layout: ${_asl_layout}"
      return ${__LOG4SH_FALSE}
      ;;
  esac

  _asl_index=`_log4sh_findArrayElement "${__log4shAppenders}" $1`
  __log4shAppenderLayouts=`_log4sh_setArrayElement \
      "${__log4shAppenderLayouts}" ${_asl_index} "${_asl_layout}"`

  # resource the appender
  _appender_cache ${_asl_appender}

  unset _asl_appender _asl_index _asl_layout
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getLayoutByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Layout of an Appender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getLayoutByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getLayoutByIndex()
{
  _log4sh_getArrayElement "${__log4shAppenderLayouts}" $1
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getLevel</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current logging Level of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getLevel myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getLevel()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_getLevel(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  agl_appender=$1

  agl_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${agl_appender}`
  # TODO: put check for valid index here
  agl_level=`_log4sh_getArrayElement \
      "${__log4shAppenderLevels}" ${agl_index}`
  __log4sh_return=$?

  echo "${agl_level}"

  unset agl_appender agl_index agl_level
  return ${__log4sh_return}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setLevel</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Level of an Appender (e.g. INFO)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setLevel myAppender INFO</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setLevel()
{
  asl_appender=$1
  asl_level=$2

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asl_appender}`
  __log4shAppenderLevels=`_log4sh_setArrayElement \
    "${__log4shAppenderLevels}" ${_index} "${asl_level}"`

  # resource the appender
  _appender_cache ${asl_appender}

  unset asl_appender asl_level _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getLevelByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current logging Level of an Appender at the given array
#   index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getLevelByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getLevelByIndex()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  _log4sh_getArrayElement "${__log4shAppenderLevels}" $1
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Pattern of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>pattern=`appender_getPattern myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getPattern()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppenderPatterns" $_index
  unset _index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Pattern of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setPattern myAppender '%d %p - %m%n'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setPattern()
{
  asp_appender=$1
  asp_pattern=$2

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asp_appender}`
  __log4shAppenderPatterns=`_log4sh_setArrayElement \
    "${__log4shAppenderPatterns}" ${_index} "${asp_pattern}"`

  # resource the appender
  _appender_cache ${asp_appender}

  unset asp_appender asp_pattern _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getPatternByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Pattern of an Appender at the specified array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>pattern=`_appender_getPatternByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getPatternByIndex()
{
  _log4sh_getArrayElement "$__log4shAppenderPatterns" $1
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_parsePattern</function></funcdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#       <paramdef>string <parameter>priority</parameter></paramdef>
#       <paramdef>string <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Generate a logging message given a Pattern, priority, and message.
#   All dates will be represented as ISO 8601 dates (YYYY-MM-DD
#   HH:MM:SS).</para>
#   <para>Note: the '<code>%r</code>' character modifier does not work in the
#   Solaris <code>/bin/sh</code> shell</para>
#   <para>Example:
#     <blockquote>
#       <funcsynopsis>
#         <funcsynopsisinfo>_appender_parsePattern '%d %p - %m%n' INFO "message to log"</funcsynopsisinfo>
#       </funcsynopsis>
#     </blockquote>
#   </para>
# </entry>
# </s:function>
#*/
_appender_parsePattern()
{
  _pattern=$1
  _priority=$2
  _msg=$3

  _date=''
  _doEval=${__LOG4SH_FALSE}

  # determine if various commands must be run
  _oldIFS="${IFS}"; IFS='%'; set -- x${_pattern}; IFS="${_oldIFS}"
  if [ $# -gt 1 ]; then
    # run the date command??
    IFS='d'; set -- ${_pattern}x; IFS="${_oldIFS}"
    [ $# -gt 1 ] && _date=`date '+%Y-%m-%d %H:%M:%S'`

    # run the eval command?
    IFS='X'; set -- ${_pattern}x; IFS="${_oldIFS}"
    [ $# -gt 1 ] && _doEval=${__LOG4SH_TRUE}
  fi
  unset _oldIFS

  # escape any '\' and '&' chars in the message
  _msg=`echo "${_msg}" |sed 's/\\\\/\\\\\\\\/g;s/&/\\\\&/g'`

  # deal with any newlines in the message
  _msg=`echo "${_msg}" |tr '\n' ''`

  # parse the pattern
  _pattern=`echo "${_pattern}" |sed \
    -e 's/%c/shell/g' \
    -e 's/%d{[^}]*}/%d/g' -e "s/%d/${_date}/g" \
    -e "s/%F/${__log4sh_filename}/g" \
    -e 's/%L//g' \
    -e 's/%n//g' \
    -e "s/%-*[0-9]*p/${_priority}/g" \
    -e "s/%-*[0-9]*r/${__log4sh_seconds}/g" \
    -e "s/%t/${__log4sh_threadName}/g" \
    -e 's/%x//g' \
    -e 's/%X{/$\{/g' \
    -e 's/%%m/%%%m/g' -e 's/%%/%/g' \
    -e "s%m${_msg}" |tr '' '\n'`
  if [ ${_doEval} -eq ${__LOG4SH_FALSE} ]; then
    echo "${_pattern}"
  else
    eval "echo \"${_pattern}\""
  fi

  unset _date _doEval _msg _pattern _tag
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Type of an Appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getType myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getType()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppenderTypes" $_index
  unset _index
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getAppenderType</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Gets the Type of an Appender at the given array index
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`appender_getAppenderType 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getAppenderType()
{
  _appender_getTypeByIndex "$@"
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>type</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the Type of an Appender (e.g. FileAppender)</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setType myAppender FileAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setType()
{
  ast_appender=$1
  ast_type=$2

  # XXX need to verify types

  _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${ast_appender}`
  __log4shAppenderTypes=`_log4sh_setArrayElement \
    "${__log4shAppenderTypes}" ${_index} "${ast_type}"`

  # resource the appender
  _appender_cache ${ast_appender}

  unset ast_appender ast_type _index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Appender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderType</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>type</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Sets the Type of an Appender (e.g. FileAppender)
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderType myAppender FileAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderType()
{
  appender_setType "$@"
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_getTypeByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the Type of an Appender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>type=`_appender_getTypeByIndex 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_getTypeByIndex()
{
  _log4sh_getArrayElement "$__log4shAppenderTypes" $1
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_cache</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Dynamically creates an appender function in memory that will fully
#   instantiate itself when it is called.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_cache myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_cache()
{
  _ac__appender=$1

  _ac__inc="${__log4sh_tmpDir}/${_ac__appender}${__LOG4SH_APPENDER_INCLUDE_EXT}"

  cat >"${_ac__inc}" <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_ac__appender}_activateOptions()
{
  [ -n "\${FUNCNAME:-}" ] && \${__LOG4SH_TRACE} "\${FUNCNAME}()\${BASH_LINENO:+'(called from \${BASH_LINENO})'}"
  _appender_activate ${_ac__appender}
}

${__LOG4SH_APPENDER_FUNC_PREFIX}${_ac__appender}_append() { :; }
EOF

  # source the new functions
  . "${_ac__inc}"

  # call the activateOptions function
  # XXX will be removed in log4sh-1.5.x
  appender_activateOptions ${_ac__appender}
}

#/**
# <s:function group="Appender" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_activate</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Dynamically regenerates an appender function in memory that is fully
#     instantiated for a specific logging task.
#     </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_activate myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_activate()
{
  [ -n "${FUNCNAME:-}" ] && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"
  ${__LOG4SH_TRACE} "_appender_activate($#)"
  _aa_appender=$1
  ${__LOG4SH_TRACE} "_aa_appender='${_aa_appender}'"

  _aa_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${_aa_appender}`
  _aa_inc="${__log4sh_tmpDir}/${_aa_appender}${__LOG4SH_APPENDER_INCLUDE_EXT}"

  ### generate function for inclusion
  # TODO can we modularize this in the future?

  # send STDOUT to our include file
  exec 4>&1 >${_aa_inc}

  # header
  cat <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_aa_appender}_append()
{
  [ -n "\${FUNCNAME:-}" ] && \${__LOG4SH_TRACE} "\${FUNCNAME}()\${BASH_LINENO:+'(called from \${BASH_LINENO})'}"
  _la_level=\$1
  _la_message=\$2
EOF

  # determine the 'layout'
  _aa_layout=`_appender_getLayoutByIndex ${_aa_index}`
  ${__LOG4SH_TRACE} "_aa_layout='${_aa_layout}'"
  case ${_aa_layout} in
    ${__LOG4SH_LAYOUT_SIMPLE}|\
    ${__LOG4SH_LAYOUT_HTML})
      ${__LOG4SH_DEBUG} 'using simple/html layout'
      echo "  _la_layout=\"\${_la_level} - \${_la_message}\""
      ;;

    ${__LOG4SH_LAYOUT_PATTERN})
      ${__LOG4SH_DEBUG} 'using pattern layout'
      _aa_pattern=`_appender_getPatternByIndex ${_aa_index}`
      echo "  _la_layout=\`_appender_parsePattern '${_aa_pattern}' \${_la_level} \"\${_la_message}\"\`"
      ;;
  esac

  # what appender 'type' do we have? TODO check not missing
  _aa_type=`_appender_getTypeByIndex ${_aa_index}`
  ${__LOG4SH_TRACE} "_aa_type='${_aa_type}'"
  case ${_aa_type} in
    ${__LOG4SH_TYPE_CONSOLE})
      echo "  echo \"\${_la_layout}\""
      ;;

    ${__LOG4SH_TYPE_FILE}|\
    ${__LOG4SH_TYPE_ROLLING_FILE}|\
    ${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
      _aa_file=`_appender_file_getFileByIndex ${_aa_index}`
      ${__LOG4SH_TRACE} "_aa_file='${_aa_file}'"
      if [ "${_aa_file}" = 'STDERR' ]; then
        echo "  echo \"\${_la_layout}\" >&2"
      elif [ "${_aa_file}" != "${__LOG4SH_NULL}" ]; then
        # do rotation
        case ${_aa_type} in
          ${__LOG4SH_TYPE_ROLLING_FILE})
            # check whether the max file size has been exceeded
            _aa_rotIndex=`appender_file_getMaxBackupIndex ${_aa_appender}`
            _aa_rotSize=`appender_file_getMaxFileSize ${_aa_appender}`
            cat <<EOF
  _la_rotSize=${_aa_rotSize}
  _la_size=\`wc -c '${_aa_file}' |awk '{print \$1}'\`
  if [ \${_la_size} -ge \${_la_rotSize} ]; then
    if [ ${_aa_rotIndex} -gt 0 ]; then
      # rotate the appender file(s)
      _la_rotIndex=`expr ${_aa_rotIndex} - 1`
      _la_rotFile="${_aa_file}.\${_la_rotIndex}"
      [ -f "\${_la_rotFile}" ] && rm -f "\${_la_rotFile}"
      while [ \${_la_rotIndex} -gt 0 ]; do
        _la_rotFileLast="\${_la_rotFile}"
        _la_rotIndex=\`expr \${_la_rotIndex} - 1\`
        _la_rotFile="${_aa_file}.\${_la_rotIndex}"
        [ -f "\${_la_rotFile}" ] && mv -f "\${_la_rotFile}" "\${_la_rotFileLast}"
      done
      mv -f '${_aa_file}' "\${_la_rotFile}"
    else
      # keep no backups; truncate the file
      cp /dev/null "${_aa_file}"
    fi
    unset _la_rotFile _la_rotFileLast _la_rotIndex
  fi
  unset _la_rotSize _la_size
EOF
            ;;
          ${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
            ;;
        esac
        echo "  echo \"\${_la_layout}\" >>'${_aa_file}'"
      else
        # the file "${__LOG4SH_NULL}" is closed?? Why did we get here, and why
        # did I care when I wrote this bit of code?
        :
      fi

      unset _aa_file
      ;;

    ${__LOG4SH_TYPE_SMTP})
      _aa_smtpTo=`appender_smtp_getTo ${_aa_appender}`
      _aa_smtpSubject=`appender_smtp_getSubject ${_aa_appender}`

      cat <<EOF
  echo "\${_la_layout}" |\\
      ${__log4sh_alternative_mail} -s "${_aa_smtpSubject}" ${_aa_smtpTo}
EOF
      ;;

    ${__LOG4SH_TYPE_SYSLOG})
      cat <<EOF
  case "\${_la_level}" in
    ${__LOG4SH_LEVEL_TRACE_STR}) _la_tag='debug' ;;  # no 'trace' equivalent
    ${__LOG4SH_LEVEL_DEBUG_STR}) _la_tag='debug' ;;
    ${__LOG4SH_LEVEL_INFO_STR}) _la_tag='info' ;;
    ${__LOG4SH_LEVEL_WARN_STR}) _la_tag='warning' ;;  # 'warn' is deprecated
    ${__LOG4SH_LEVEL_ERROR_STR}) _la_tag='err' ;;     # 'error' is deprecated
    ${__LOG4SH_LEVEL_FATAL_STR}) _la_tag='alert' ;;
  esac
EOF

      _aa_facilityName=`appender_syslog_getFacility ${_aa_appender}`
      _aa_syslogHost=`appender_syslog_getHost ${_aa_appender}`
      _aa_hostname=`hostname |sed 's/^\([^.]*\)\..*/\1/'`

      # are we logging to a remote host?
      if [ -z "${_aa_syslogHost}" ]; then
        # no -- use logger
        cat <<EOF
  ( exec logger -p "${_aa_facilityName}.\${_la_tag}" \
      -t "${__log4sh_filename}[$$]" "\${_la_layout}" 2>/dev/null )
  unset _la_tag
EOF
      else
        # yes -- use netcat
        if [ -n "${__log4sh_alternative_nc:-}" ]; then
          case ${_aa_facilityName} in
            kern) _aa_facilityCode=0 ;;            # 0<<3
            user) _aa_facilityCode=8 ;;            # 1<<3
            mail) _aa_facilityCode=16 ;;           # 2<<3
            daemon) _aa_facilityCode=24 ;;         # 3<<3
            auth|security) _aa_facilityCode=32 ;;  # 4<<3
            syslog) _aa_facilityCode=40 ;;         # 5<<3
            lpr) _aa_facilityCode=48 ;;            # 6<<3
            news) _aa_facilityCode=56 ;;           # 7<<3
            uucp) _aa_facilityCode=64 ;;           # 8<<3
            cron) _aa_facilityCode=72 ;;           # 9<<3
            authpriv) _aa_facilityCode=80 ;;       # 10<<3
            ftp) _aa_facilityCode=88 ;;            # 11<<3
            local0) _aa_facilityCode=128 ;;        # 16<<3
            local1) _aa_facilityCode=136 ;;        # 17<<3
            local2) _aa_facilityCode=144 ;;        # 18<<3
            local3) _aa_facilityCode=152 ;;        # 19<<3
            local4) _aa_facilityCode=160 ;;        # 20<<3
            local5) _aa_facilityCode=168 ;;        # 21<<3
            local6) _aa_facilityCode=176 ;;        # 22<<3
            local7) _aa_facilityCode=184 ;;        # 23<<3
          esac

          cat <<EOF
  case \${_la_tag} in
    alert) _la_priority=1 ;;
    err|error) _la_priority=3 ;;
    warning|warn) _la_priority=4 ;;
    info) _la_priority=6 ;;
    debug) _la_priority=7 ;;
  esac
  _la_priority=\`expr ${_aa_facilityCode} + \${_la_priority}\`
  _la_date=\`date "+%b %d %H:%M:%S"\`
  _la_hostname='${_aa_hostname}'

  _la_syslogMsg="<\${_la_priority}>\${_la_date} \${_la_hostname} \${_la_layout}"

  # do RFC 3164 cleanups
  _la_date=\`echo \"\${_la_date}\" |sed 's/ 0\([0-9]\) /  \1 /'\`
  _la_syslogMsg=\`echo "\${_la_syslogMsg}" |cut -b1-1024\`

  ( echo "\${_la_syslogMsg}" |\
      exec ${__log4sh_alternative_nc} ${__log4sh_alternative_nc_opts} -w 1 -u \
          ${_aa_syslogHost} 514 )
  unset _la_tag _la_priority _la_date _la_hostname _la_syslogMsg
EOF
          unset _aa_facilityCode _aa_syslogHost _aa_hostname
        else
          # no netcat alternative set; doing nothing
          :
        fi
      fi
      unset _aa_facilityName
      ;;

    *) _log4sh_error "unrecognized appender type (${_aa_type})" ;;
  esac

  # footer
  cat <<EOF
  unset _la_level _la_message _la_layout
}
EOF

  # override the activateOptions function as we don't need it anymore
  cat <<EOF
${__LOG4SH_APPENDER_FUNC_PREFIX}${_aa_appender}_activateOptions() { :; }
EOF

  # restore STDOUT
  exec 1>&4 4>&-

  # source the newly created function
  ${__LOG4SH_TRACE} 're-sourcing the newly created function'
  . "${_aa_inc}"

  unset _aa_appender _aa_inc _aa_layout _aa_pattern _aa_type
}

#-----------------------------------------------------------------------------
# FileAppender
#

#/**
# <s:function group="FileAppender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_appender_file_getFileByIndex</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the filename of a FileAppender at the given array index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_appender_file_getFileByIndex 3</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_file_getFileByIndex()
{
  _log4sh_getArrayElement "${__log4shAppender_file_files}" $1
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the filename of a FileAppender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_getFile myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getFile()
{
  _index=`_log4sh_findArrayElement "$__log4shAppenders" $1`
  _log4sh_getArrayElement "$__log4shAppender_file_files" $_index
  unset _index
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the filename for a FileAppender (e.g. <filename>STDERR</filename> or
#     <filename>/var/log/log4sh.log</filename>).
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setFile myAppender STDERR</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setFile()
{
  afsf_appender=$1
  afsf_file=$2
  ${__LOG4SH_TRACE} "afsf_appender='${afsf_appender}' afsf_file='${afsf_file}'"

  if [ -n "${afsf_appender}" -a -n "${afsf_file}" ]; then
    # set the file
    _index=`_log4sh_findArrayElement "${__log4shAppenders}" ${afsf_appender}`
    __log4shAppender_file_files=`_log4sh_setArrayElement \
      "${__log4shAppender_file_files}" ${_index} "${afsf_file}"`
    _return=$?

    # create the file (if it isn't already)
    if [ ${_return} -eq ${__LOG4SH_TRUE} \
      -a ! "${afsf_file}" '=' "${__LOG4SH_NULL}" \
      -a ! "${afsf_file}" '=' 'STDERR' \
      -a ! -f "${afsf_file}" \
    ]; then
      touch "${afsf_file}" 2>/dev/null
      _result=$?
      # determine success of touch command
      if [ ${_result} -eq 1 ]; then
        _log4sh_error "appender_file_setFile(): could not create file (${afsf_file}); closing appender"
        appender_setLevel ${afsf_appender} ${__LOG4SH_LEVEL_CLOSED_STR}
      fi
      unset _result
    fi
  else
    _log4sh_error 'appender_file_setFile(): missing appender and/or file'
    _return=${__LOG4SH_FALSE}
  fi

  # resource the appender
  _appender_cache ${afsf_appender}

  unset afsf_appender afsf_file _index
  return ${_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderFile</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.2</emphasis></para>
#   <para>
#     Set the filename for a FileAppender (e.g. "STDERR" or
#     "/var/log/log4sh.log")
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderFile myAppender STDERR</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderFile()
{
  appender_file_setFile "$@"
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>integer</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getMaxBackupIndex</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Returns the value of the MaxBackupIndex option.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_getMaxBackupIndex myAppender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getMaxBackupIndex()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_file_getMaxBackupIndex(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  afgmbi_appender=$1

  afgmbi_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afgmbi_appender}`
  # TODO: put check for valid index here
  _log4sh_getArrayElement \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" ${afgmbi_index}
  __log4sh_return=$?

  unset afgmbi_appender afgmbi_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setMaxBackupIndex</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the maximum number of backup files to keep around.</para>
#   <para>
#     The <emphasis role="strong">MaxBackupIndex</emphasis> option determines
#     how many backup files are kept before the oldest is erased. This option
#     takes a positive integer value. If set to zero, then there will be no
#     backup files and the log file will be truncated when it reaches
#     <option>MaxFileSize</option>.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setMaxBackupIndex myAppender 3</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setMaxBackupIndex()
{
  if [ $# -ne 2 ]; then
    _log4sh_error "appender_file_setMaxBackupIndex(): invalid number of parameters ($#)"
    return ${__LOG4SH_FALSE}
  fi

  afsmbi_appender=$1
  afsmbi_maxIndex=$2

  # TODO: put check for valid input

  afsmbi_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afsmbi_appender}`
  # TODO: put check for valid index here
  __log4shAppender_rollingFile_maxBackupIndexes=`_log4sh_setArrayElement \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" ${afsmbi_index} \
      "${afsmbi_maxIndex}"`
  __log4sh_return=$?

  # re-source the appender
  _appender_cache ${afsmbi_appender}

  unset afsmbi_appender afsmbi_maxIndex afsmbi_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <code>integer</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_getMaxFileSize</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the maximum size that the output file is allowed to reach before
#     being rolled over to backup files.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>maxSize=`appender_file_getMaxBackupSize myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_getMaxFileSize()
{
  if [ $# -ne 1 ]; then
    _log4sh_error "appender_file_getMaxFileSize(): invalid number of parameters ($#)"
    return ${__LOG4SH_FALSE}
  fi

  afgmfs_appender=$1

  afgmfs_index=`_log4sh_findArrayElement \
      "${__log4shAppenders}" ${afgmfs_appender}`
  # TODO: put check for valid index here
  _log4sh_getArrayElement \
      "${__log4shAppender_rollingFile_maxFileSizes}" ${afgmfs_index}
  __log4sh_return=$?

  unset afgmfs_appender afgmfs_index
  return ${__log4sh_return}
}

#/**
# <s:function group="FileAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_file_setMaxFileSize</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>size</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the maximum size that the output file is allowed to reach before
#     being rolled over to backup files.
#   </para>
#   <para>
#     In configuration files, the <option>MaxFileSize</option> option takes an
#     long integer in the range 0 - 2^40. You can specify the value with the
#     suffixes "KiB", "MiB" or "GiB" so that the integer is interpreted being
#     expressed respectively in kilobytes, megabytes or gigabytes. For example,
#     the value "10KiB" will be interpreted as 10240.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_file_setMaxBackupSize myAppender 10KiB</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_file_setMaxFileSize()
{
  if [ $# -ne 2 ]; then
    _log4sh_error \
        "appender_file_setMaxFileSize(): invalid number of parameters ($#)"
    return ${__LOG4SH_ERROR}
  fi

  afsmfs_appender=$1
  afsmfs_size=$2
  afsmfs_return=${__LOG4SH_TRUE}

  # split the file size into parts
  afsmfs_value=`expr ${afsmfs_size} : '\([0-9]*\)'`
  afsmfs_unit=`expr ${afsmfs_size} : '[0-9]* *\([A-Za-z]\{1,3\}\)'`

  # determine multiplier
  if [ ${__log4sh_wa_strictBehavior} -eq ${__LOG4SH_TRUE} ]; then
    case "${afsmfs_unit}" in
      KB) afsmfs_unit='KiB' ;;
      MB) afsmfs_unit='MiB' ;;
      GB) afsmfs_unit='GiB' ;;
      TB) afsmfs_unit='TiB' ;;
    esac
  fi
  case "${afsmfs_unit}" in
    B) afsmfs_mul=1 ;;
    KB) afsmfs_mul=1000 ;;
    KiB) afsmfs_mul=1024 ;;
    MB) afsmfs_mul=1000000 ;;
    MiB) afsmfs_mul=1048576 ;;
    GB) afsmfs_mul=1000000000 ;;
    GiB) afsmfs_mul=1073741824 ;;
    TB) afsmfs_mul=1000000000000 ;;
    TiB) afsmfs_mul=1099511627776 ;;
    '')
      _log4sh_warn 'missing file size unit; assuming bytes'
      afsmfs_mul=1
      ;;
    *)
      _log4sh_error "unrecognized file size unit '${afsmfs_unit}'"
      afsmfs_return=${__LOG4SH_ERROR}
      ;;
  esac

  # calculate maximum file size
  if [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ]; then
    afsmfs_maxFileSize=`(expr ${afsmfs_value} \* ${afsmfs_mul} 2>&1)`
    if [ $? -gt 0 ]; then
      _log4sh_error "problem calculating maximum file size: '${afsmfs_maxFileSize}'"
      afsmfs_return=${__LOG4SH_FALSE}
    fi
  fi

  # store the maximum file size
  if [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ]; then
    afsmfs_index=`_log4sh_findArrayElement \
        "${__log4shAppenders}" ${afsmfs_appender}`
    # TODO: put check for valid index here
    __log4shAppender_rollingFile_maxFileSizes=`_log4sh_setArrayElement \
        "${__log4shAppender_rollingFile_maxFileSizes}" ${afsmfs_index} \
        "${afsmfs_maxFileSize}"`
  fi

  # re-source the appender
  [ ${afsmfs_return} -eq ${__LOG4SH_TRUE} ] \
      && _appender_cache ${afsmfs_appender}

  __log4sh_return=${afsmfs_return}
  unset afsmfs_appender afsmfs_size afsmfs_value afsmfs_unit afsmfs_mul \
      afsmfs_maxFileSize afsmfs_index afsmfs_return
  return ${__log4sh_return}
}

#-----------------------------------------------------------------------------
# SMTPAppender
#

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_getTo</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the to address for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>email=`appender_smtp_getTo myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_getTo()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_smtp_getTo(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgt_appender=$1

  asgt_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgt_appender}`
  # TODO: put check for valid index here
  asgt_to=`_log4sh_getArrayElement \
      "${__log4shAppender_smtp_tos}" ${asgt_index}`
  __log4sh_return=$?

  [ "${asgt_to}" = "${__LOG4SH_NULL}" ] && asgt_to=''
  echo "${asgt_to}"

  unset asgt_appender asgt_index asgt_to
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_setTo</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>email</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the to address for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setTo myAppender user@example.com</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_setTo()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_smtp_setTo(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asst_appender=$1
  asst_email=$2

  asst_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asst_appender}`
  # TODO: put check for valid index here
  __log4shAppender_smtp_tos=`_log4sh_setArrayElement \
    "${__log4shAppender_smtp_tos}" ${asst_index} "${asst_email}"`

  # resource the appender
  _appender_cache ${asst_appender}

  unset asst_appender asst_email asst_index
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderRecipient</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>email</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Set the to address for the given appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setTo myAppender user@example.com</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderRecipient()
{
  appender_smtp_setTo "$@"
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_getSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the email subject for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>subject=`appender_smtp_getSubject myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_getSubject()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_smtp_getSubject(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgs_appender=$1

  asgs_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgs_appender}`
  # TODO: put check for valid index here
  asgs_subject=`_log4sh_getArrayElement \
      "${__log4shAppender_smtp_subjects}" ${asgs_index}`
  __log4sh_return=$?

  [ "${asgs_subject}" = "${__LOG4SH_NULL}" ] && asgs_subject=''
  echo "${asgs_subject}"

  unset asgs_appender asgs_index asgs_subject
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_smtp_setSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>subject</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the email subject for an SMTP appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_smtp_setSubject myAppender "This is a test"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_smtp_setSubject()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_smtp_setSubject(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asss_appender=$1
  asss_subject=$2

  # set the Subject
  asss_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asss_appender}`
  if [ ${asss_index} -gt 0 ]; then
    __log4shAppender_smtp_subjects=`_log4sh_setArrayElement \
      "${__log4shAppender_smtp_subjects}" ${asss_index} "${asss_subject}"`
    __log4sh_return=${__LOG4SH_TRUE}
  else
    _log4sh_error "could not set Subject for appender (${asss_appender})"
    __log4sh_return=${__LOG4SH_FALSE}
  fi

  # re-source the appender
  _appender_cache ${asss_appender}

  unset asss_appender asss_subject asss_index
  return ${__log4sh_return}
}

#/**
# <s:function group="SMTPAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setAppenderSubject</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>subject</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Sets the email subject for an SMTP appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setAppenderSubject myAppender "This is a test"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setAppenderSubject()
{
  appender_smtp_setSubject "$@"
}

#-----------------------------------------------------------------------------
# SyslogAppender
#

#/**
# <s:function group="SyslogAppender" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef>
#         <function>_appender_syslog_getFacilityByIndex</function>
#       </funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the syslog facility of the specified appender by index</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>
#       facility=`_appender_syslog_getFacilityByIndex 3`
#     </funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_appender_syslog_getFacilityByIndex()
{
  _log4sh_getArrayElement "$__log4shAppender_syslog_facilities" $1
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_getSyslogFacility</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.1</emphasis></para>
#   <para>
#     Get the syslog facility of the specified appender by index
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>facility=`appender_getSyslogFacility 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_getSyslogFacility()
{
  _appender_syslog_getFacilityByIndex "$@"
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_getFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the syslog facility for the given appender.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>facility=`appender_syslog_getFacility myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_getFacility()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_syslog_getFacility(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgf_appender=$1

  asgf_index=`_log4sh_findArrayElement "$__log4shAppenders" ${asgf_appender}`
  _log4sh_getArrayElement "${__log4shAppender_syslog_facilities}" ${asgf_index}

  unset asgf_appender asgf_index
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_setFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>facility</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the syslog facility for the given appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_syslog_setFacility myAppender local4`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_setFacility()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_syslog_setFacility(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi
  assf_appender=$1
  assf_facility=$2

  # check for valid facility
  echo "${__LOG4SH_TYPE_SYSLOG_FACILITY_NAMES}" |grep " ${assf_facility} " >/dev/null
  if [ $? -ne 0 ]; then
    # the facility is not valid
    _log4sh_error "[${assf_facility}] is an unknown syslog facility. Defaulting to [user]."
    assf_facility='user'
  fi

  # set appender facility
  assf_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${assf_appender}`
  # TODO: put check for valid index here
  __log4shAppender_syslog_facilities=`_log4sh_setArrayElement \
    "${__log4shAppender_syslog_facilities}" ${assf_index} "${assf_facility}"`

  # re-source the appender
  _appender_cache ${assf_appender}

  unset assf_appender assf_facility assf_index
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_setSyslogFacility</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>facility</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.2</emphasis></para>
#   <para>
#     Set the syslog facility for the given appender
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_setSyslogFacility myAppender local4`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_setSyslogFacility()
{
  appender_syslog_setFacility "$@"
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <code>string</code>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_getHost</function></funcdef>
#       <paramdef>integer <parameter>index</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the syslog host of the specified appender.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>host=`appender_syslog_getHost myAppender`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
appender_syslog_getHost()
{
  if [ $# -ne 1 ]; then
    _log4sh_error 'appender_syslog_getHost(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  asgh_appender=$1

  asgh_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${asgh_appender}`
  # TODO: put check for valid index here
  asgh_host=`_log4sh_getArrayElement \
      "${__log4shAppender_syslog_hosts}" ${asgh_index}`
  __log4sh_return=$?

  [ "${asgh_host}" = "${__LOG4SH_NULL}" ] && asgh_host=''
  echo "${asgh_host}"

  unset asgh_appender asgh_index asgh_host
  return ${__log4sh_return}
}

#/**
# <s:function group="SyslogAppender" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>appender_syslog_setHost</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>host</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Set the syslog host for the given appender. Requires that the 'nc'
#     command alternative has been previously set with the
#     log4sh_setAlternative() function.
#   </para>
#   <para><emphasis role="strong">Since:</emphasis> 1.3.7</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>appender_syslog_setHost myAppender localhost</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
#
# The BSD syslog Protocol
#   http://www.ietf.org/rfc/rfc3164.txt
#
appender_syslog_setHost()
{
  if [ $# -ne 2 ]; then
    _log4sh_error 'appender_syslog_setHost(): invalid number of parameters'
    return ${__LOG4SH_FALSE}
  fi

  assh_appender=$1
  assh_host=$2

  [ -z "${__log4sh_alternative_nc:-}" ] \
      && _log4sh_warn 'the nc (netcat) command alternative is required for remote syslog logging. see log4sh_setAlternative().'

  assh_index=`_log4sh_findArrayElement "${__log4shAppenders}" ${assh_appender}`
  # TODO: put check for valid index here
  __log4shAppender_syslog_hosts=`_log4sh_setArrayElement \
      "${__log4shAppender_syslog_hosts}" ${assh_index} "${assh_host}"`

  # re-source the appender
  _appender_cache ${assh_appender}

  unset assh_appender assh_host assh_index
  return ${__LOG4SH_TRUE}
}

#=============================================================================
# Level
#

#/**
# <s:function group="Level" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_level_toLevel</function></funcdef>
#       <paramdef>integer <parameter>val</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Converts an internally used level integer into its external level
#   equivalent</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>level=`logger_level_toLevel 3`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
# TODO use arrays instead of case statement ??
logger_level_toLevel()
{
  _ltl__val=$1

  _ltl__return=${__LOG4SH_TRUE}
  _ltl__level=''

  case ${_ltl__val} in
    ${__LOG4SH_LEVEL_TRACE}) _ltl__level=${__LOG4SH_LEVEL_TRACE_STR} ;;
    ${__LOG4SH_LEVEL_DEBUG}) _ltl__level=${__LOG4SH_LEVEL_DEBUG_STR} ;;
    ${__LOG4SH_LEVEL_INFO}) _ltl__level=${__LOG4SH_LEVEL_INFO_STR} ;;
    ${__LOG4SH_LEVEL_WARN}) _ltl__level=${__LOG4SH_LEVEL_WARN_STR} ;;
    ${__LOG4SH_LEVEL_ERROR}) _ltl__level=${__LOG4SH_LEVEL_ERROR_STR} ;;
    ${__LOG4SH_LEVEL_FATAL}) _ltl__level=${__LOG4SH_LEVEL_FATAL_STR} ;;
    ${__LOG4SH_LEVEL_OFF}) _ltl__level=${__LOG4SH_LEVEL_OFF_STR} ;;
    ${__LOG4SH_LEVEL_CLOSED}) _ltl__level=${__LOG4SH_LEVEL_CLOSED_STR} ;;
    *) _ltl__return=${__LOG4SH_FALSE} ;;
  esac

  echo ${_ltl__level}
  unset _ltl__val _ltl__level
  return ${_ltl__return}
}

#/**
# <s:function group="Level" modifier="public">
# <entry align="right">
#   <code>integer</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_level_toInt</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Converts an externally used level tag into its integer
#   equivalent</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>levelInt=`logger_level_toInt WARN`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_level_toInt()
{
  _lti__level=$1

  _lti__int=0
  _lti__return=${__LOG4SH_TRUE}

  case ${_lti__level} in
    ${__LOG4SH_LEVEL_TRACE_STR}) _lti__int=${__LOG4SH_LEVEL_TRACE} ;;
    ${__LOG4SH_LEVEL_DEBUG_STR}) _lti__int=${__LOG4SH_LEVEL_DEBUG} ;;
    ${__LOG4SH_LEVEL_INFO_STR}) _lti__int=${__LOG4SH_LEVEL_INFO} ;;
    ${__LOG4SH_LEVEL_WARN_STR}) _lti__int=${__LOG4SH_LEVEL_WARN} ;;
    ${__LOG4SH_LEVEL_ERROR_STR}) _lti__int=${__LOG4SH_LEVEL_ERROR} ;;
    ${__LOG4SH_LEVEL_FATAL_STR}) _lti__int=${__LOG4SH_LEVEL_FATAL} ;;
    ${__LOG4SH_LEVEL_OFF_STR}) _lti__int=${__LOG4SH_LEVEL_OFF} ;;
    ${__LOG4SH_LEVEL_CLOSED_STR}) _lti__int=${__LOG4SH_LEVEL_CLOSED} ;;
    *) _lti__return=${__LOG4SH_FALSE} ;;
  esac

  echo ${_lti__int}
  unset _lti__int _lti__level
  return ${_lti__return}
}

#=============================================================================
# Logger
#

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/<code>boolean</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_addAppender</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Add and initialize a new appender</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_addAppender $appender</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_addAppender()
{
  laa_appender=$1

  # FAQ should we be using setter functions here?? for performance, no.
  __log4shAppenders=`_log4sh_pushStack "${__log4shAppenders}" ${laa_appender}`
  __log4shAppenderCount=`expr ${__log4shAppenderCount} + 1`
  __log4shAppenderCounts="${__log4shAppenderCounts} ${__log4shAppenderCount}"
  __log4shAppenderLayouts=`_log4sh_pushStack \
      "$__log4shAppenderLayouts" "${__LOG4SH_LAYOUT_SIMPLE}"`
  __log4shAppenderLevels=`_log4sh_pushStack \
      "${__log4shAppenderLevels}" "${__LOG4SH_NULL}"`
  __log4shAppenderPatterns=`_log4sh_pushStack \
      "${__log4shAppenderPatterns}" "${__LOG4SH_PATTERN_DEFAULT}"`
  __log4shAppenderTypes=`_log4sh_pushStack \
      "${__log4shAppenderTypes}" ${__LOG4SH_TYPE_CONSOLE}`
  __log4shAppender_file_files=`_log4sh_pushStack \
      "${__log4shAppender_file_files}" ${__LOG4SH_NULL}`
  __log4shAppender_rollingFile_maxBackupIndexes=`_log4sh_pushStack \
      "${__log4shAppender_rollingFile_maxBackupIndexes}" \
      ${__LOG4SH_TYPE_ROLLING_FILE_MAX_BACKUP_INDEX}`
  __log4shAppender_rollingFile_maxFileSizes=`_log4sh_pushStack \
      "${__log4shAppender_rollingFile_maxFileSizes}" \
      ${__LOG4SH_TYPE_ROLLING_FILE_MAX_FILE_SIZE}`
  __log4shAppender_smtp_tos=`_log4sh_pushStack \
      "${__log4shAppender_smtp_tos}" ${__LOG4SH_NULL}`
  __log4shAppender_smtp_subjects=`_log4sh_pushStack \
      "${__log4shAppender_smtp_subjects}" ${__LOG4SH_NULL}`
  __log4shAppender_syslog_facilities=`_log4sh_pushStack \
      "${__log4shAppender_syslog_facilities}" ${__LOG4SH_TYPE_SYSLOG_FACILITY}`
  __log4shAppender_syslog_hosts=`_log4sh_pushStack \
      "${__log4shAppender_syslog_hosts}" "${__LOG4SH_NULL}"`

  _appender_cache ${laa_appender}

  unset laa_appender
  return ${__LOG4SH_TRUE}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_addAppenderWithPattern</function></funcdef>
#       <paramdef>string <parameter>appender</parameter></paramdef>
#       <paramdef>string <parameter>pattern</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.6</emphasis></para>
#   <para>
#     Add and initialize a new appender with a specific PatternLayout
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_addAppenderWithPattern $appender '%d %p - %m%n'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_addAppenderWithPattern()
{
  _myAppender=$1
  _myPattern=$2

  logger_addAppender ${_myAppender}
  appender_setLayout ${_myAppender} ${__LOG4SH_LAYOUT_PATTERN}
  appender_setPattern ${_myAppender} "${_myPattern}"

  unset _myAppender _myPattern
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getFilename</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Get the filename that would be shown when the '%F' conversion character
#     is used in a PatternLayout.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>filename=`logger_getFilename`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getFilename()
{
  echo "${__log4sh_filename}"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setFilename</function></funcdef>
#       <paramdef>string <parameter>filename</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Set the filename to be shown when the '%F' conversion character is
#   used in a PatternLayout.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setFilename 'myScript.sh'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setFilename()
{
  __log4sh_filename=$1
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getLevel</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>Get the global default logging level (e.g. DEBUG).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>level=`logger_getLevel`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getLevel()
{
  logger_level_toLevel ${__log4shLevel}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setLevel</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Sets the global default logging level (e.g. DEBUG).</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setLevel INFO</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setLevel()
{
  _l_level=$1

  _l_int=`logger_level_toInt ${_l_level}`
  if [ $? -eq ${__LOG4SH_TRUE} ]; then
    __log4shLevel=${_l_int}
  else
    _log4sh_error "attempt to set invalid log level '${_l_level}'"
  fi

  unset _l_int _l_level
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log</function></funcdef>
#       <paramdef>string <parameter>level</parameter></paramdef>
#       <paramdef>string[] <parameter>message(s)</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>The base logging command that logs a message to all defined
#     appenders</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log DEBUG 'This is a test message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log()
{
  _l_level=$1
  shift
  # if no message was passed, read it from STDIN
  [ $# -ne 0 ] && _l_msg="$@" || _l_msg=`cat`

  __log4sh_return=${__LOG4SH_TRUE}
  _l_levelInt=`logger_level_toInt ${_l_level}`
  if [ $? -eq ${__LOG4SH_TRUE} ]; then
    # update seconds elapsed
    _log4sh_updateSeconds

    _l_oldIFS=${IFS} IFS=${__LOG4SH_IFS_DEFAULT}
    for _l_appenderIndex in ${__log4shAppenderCounts}; do
      ${__LOG4SH_TRACE} "_l_appenderIndex='${_l_appenderIndex}'"
      # determine appender level
      _l_appenderLevel=`_appender_getLevelByIndex ${_l_appenderIndex}`
      if [ "${_l_appenderLevel}" = "${__LOG4SH_NULL}" ]; then
        # continue if requested is level less than general level
        [ ! ${__log4shLevel} -le ${_l_levelInt} ] && continue
      else
        _l_appenderLevelInt=`logger_level_toInt ${_l_appenderLevel}`
        # continue if requested level is less than specific appender level
        ${__LOG4SH_TRACE} "_l_levelInt='${_l_levelInt}' _l_appenderLevelInt='${_l_appenderLevelInt}'"
        [ ! ${_l_appenderLevelInt} -le ${_l_levelInt} ] && continue
      fi

      # execute dynamic appender function
      _l_appenderName=`_log4sh_getArrayElement \
        "${__log4shAppenders}" ${_l_appenderIndex}`
      ${__LOG4SH_APPENDER_FUNC_PREFIX}${_l_appenderName}_append ${_l_level} "${_l_msg}"
    done
    IFS=${_l_oldIFS}
  else
    _log4sh_error "invalid logging level requested (${_l_level})"
    __log4sh_return=${__LOG4SH_ERROR}
  fi

  unset _l_msg _l_oldIFS _l_level _l_levelInt
  unset _l_appenderIndex _l_appenderLevel _l_appenderLevelInt _l_appenderName
  return ${__log4sh_return}
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_trace</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the TRACE
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_trace 'This is a trace message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_trace()
{
  log ${__LOG4SH_LEVEL_TRACE_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_debug</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the DEBUG
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_debug 'This is a debug message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_debug()
{
  log ${__LOG4SH_LEVEL_DEBUG_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_info</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the INFO
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_info 'This is a info message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_info()
{
  log ${__LOG4SH_LEVEL_INFO_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_warn</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is a helper function for logging a message at the WARN priority
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_warn 'This is a warn message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_warn()
{
  log ${__LOG4SH_LEVEL_WARN_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_error</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This is a helper function for logging a message at the ERROR priority
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_error 'This is a error message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_error()
{
  log ${__LOG4SH_LEVEL_ERROR_STR} "$@"
}

#/**
# <s:function group="Logger" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_fatal</function></funcdef>
#       <paramdef>string[] <parameter>message</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a helper function for logging a message at the FATAL
#     priority</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_fatal 'This is a fatal message'</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_fatal()
{
  log ${__LOG4SH_LEVEL_FATAL_STR} "$@"
}

#==============================================================================
# Property
#

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_getPropPrefix</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Takes a string (eg. "log4sh.appender.stderr.File") and returns the
#   prefix of it (everything before the first '.' char). Normally used in
#   parsing the log4sh configuration file.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>prefix=`_log4sh_getPropPrefix $property"`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_getPropPrefix()
{
  _oldIFS=${IFS} IFS='.'
  set -- $1
  IFS=${_oldIFS} unset _oldIFS
  echo $1
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_stripPropPrefix</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Strips the prefix off a property configuration command and returns
#   the string. E.g. "log4sh.appender.stderr.File" becomes
#   "appender.stderr.File".</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>newProperty=`_log4sh_stripPropPrefix $property`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_stripPropPrefix()
{
  expr "$1" : '[^.]*\.\(.*\)'
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propAlternative</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Configures log4sh to use an alternative command.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propAlternative property value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propAlternative()
{
  _lpa_key=$1
  _lpa_value=$2

  # strip the leading 'alternative.'
  _lpa_alternative=`_log4sh_stripPropPrefix ${_lpa_key}`

  # set the alternative
  log4sh_setAlternative ${_lpa_alternative} "${_lpa_value}"

  unset _lpa_key _lpa_value _lpa_alternative
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propAppender</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Configures log4sh using an appender property configuration statement</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propAppender $property $value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propAppender()
{
  _lpa_key=$1
  _lpa_value=$2

  _lpa_appender=''
  _lpa_rtrn=${__LOG4SH_TRUE}

  # strip the leading 'appender' keyword prefix
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`

  # handle appender definitions
  if [ "${_lpa_key}" '=' "`expr \"${_lpa_key}\" : '\([^.]*\)'`" ]; then
    _lpa_appender="${_lpa_key}"
  else
    _lpa_appender=`_log4sh_getPropPrefix ${_lpa_key}`
  fi

  # does the appender exist?
  appender_exists ${_lpa_appender}
  if [ $? -eq ${__LOG4SH_FALSE} ]; then
    _log4sh_error "attempt to configure the non-existant appender (${_lpa_appender})"
    unset _lpa_appender _lpa_key _lpa_value
    return ${__LOG4SH_ERROR}
  fi

  # handle the appender type
  if [ "${_lpa_appender}" = "${_lpa_key}" ]; then
    case ${_lpa_value} in
      ${__LOG4SH_TYPE_CONSOLE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_CONSOLE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_CONSOLE} ;;
      ${__LOG4SH_TYPE_FILE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_FILE} ;;
      $__LOG4SH_TYPE_DAILY_ROLLING_FILE|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_DAILY_ROLLING_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_DAILY_ROLLING_FILE} ;;
      ${__LOG4SH_TYPE_ROLLING_FILE}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_ROLLING_FILE})
        appender_setType ${_lpa_appender} ${__LOG4SH_TYPE_ROLLING_FILE} ;;
      ${__LOG4SH_TYPE_SMTP}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_SMTP})
        appender_setType $_lpa_appender ${__LOG4SH_TYPE_SMTP} ;;
      ${__LOG4SH_TYPE_SYSLOG}|\
      ${__LOG4SH_CONFIG_LOG4J_CP}.${__LOG4SH_TYPE_SYSLOG})
        appender_setType $_lpa_appender ${__LOG4SH_TYPE_SYSLOG} ;;
      *)
        _log4sh_error "appender type (${_lpa_value}) unrecognized"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
    __log4sh_return=${_lpa_rtrn}
    unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
    return ${__log4sh_return}
  fi

  # handle appender values and methods
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`
  if [ "${_lpa_key}" '=' "`expr \"${_lpa_key}\" : '\([^.]*\)'`" ]; then
    case ${_lpa_key} in
      # General
      Threshold) appender_setLevel ${_lpa_appender} "${_lpa_value}" ;;
      layout) appender_setLayout ${_lpa_appender} "${_lpa_value}" ;;

      # FileAppender
      DatePattern) ;;  # unsupported
      File)
        _lpa_value=`eval echo "${_lpa_value}"`
        appender_file_setFile ${_lpa_appender} "${_lpa_value}"
        ;;
      MaxBackupIndex)
        appender_file_setMaxBackupIndex ${_lpa_appender} "${_lpa_value}" ;;
      MaxFileSize)
        appender_file_setMaxFileSize ${_lpa_appender} "${_lpa_value}" ;;

      # SMTPAppender
      To) appender_smtp_setTo ${_lpa_appender} "${_lpa_value}" ;;
      Subject) appender_smtp_setSubject ${_lpa_appender} "${_lpa_value}" ;;

      # SyslogAppender
      SyslogHost) appender_syslog_setHost ${_lpa_appender} "${_lpa_value}" ;;
      Facility) appender_syslog_setFacility ${_lpa_appender} "${_lpa_value}" ;;

      # catch unrecognized
      *)
        _log4sh_error "appender value/method (${_lpa_key}) unrecognized"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
    __log4sh_return=${_lpa_rtrn}
    unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
    return ${__log4sh_return}
  fi

  # handle appender layout values and methods
  _lpa_key=`_log4sh_stripPropPrefix ${_lpa_key}`
  case ${_lpa_key} in
    ConversionPattern) appender_setPattern ${_lpa_appender} "${_lpa_value}" ;;
    *)
      _log4sh_error "layout value/method (${_lpa_key}) unrecognized"
      false
      ;;
  esac
  [ $? -ne ${__LOG4SH_TRUE} ] && _lpa_rtrn=${__LOG4SH_ERROR}
  __log4sh_return=${_lpa_rtrn}
  unset _lpa_appender _lpa_key _lpa_rtrn _lpa_value
  return ${__log4sh_return}
}

#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propLogger</function></funcdef>
#       <paramdef>string <parameter>property</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>(future) Configures log4sh with a <code>logger</code> configuration
#   statement. Sample output: "logger: property value".</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>result=`_log4sh_propLogger $property $value`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propLogger()
{
  _prop=`_log4sh_stripPropPrefix $1`
  echo "logger: ${_prop} $2"
  unset _prop
}

#
# configure log4sh with a rootLogger configuration statement
#
# @param  _key    configuration command
# @param  _value  configuration value
#
#/**
# <s:function group="Property" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_propRootLogger</function></funcdef>
#       <paramdef>string <parameter>rootLogger</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Configures log4sh with a <code>rootLogger</code> configuration
#   statement. It expects a comma separated string similar to the following:</para>
#   <para><code>log4sh.rootLogger=ERROR, stderr, R</code></para>
#   <para>The first option is the default logging level to set for all
#   of the following appenders that will be created, and all following options
#   are the names of appenders to create. The appender names must be
#   unique.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_propRootLogger $value</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_propRootLogger()
{
  __lprl_rootLogger=`echo "$@" |sed 's/ *, */,/g'`
  __lprl_count=`echo "${__lprl_rootLogger}" |sed 's/,/ /g' |wc -w`
  __lprl_index=1
  while [ ${__lprl_index} -le ${__lprl_count} ]; do
    __lprl_operand=`echo "${__lprl_rootLogger}" |cut -d, -f${__lprl_index}`
    if [ ${__lprl_index} -eq 1 ]; then
      logger_setLevel "${__lprl_operand}"
    else
      appender_exists "${__lprl_operand}"
      if [ $? -eq ${__LOG4SH_FALSE} ]; then
        logger_addAppender "${__lprl_operand}"
      else
        _log4sh_error "attempt to add already existing appender of name (${__lprl_operand})"
      fi
    fi
    __lprl_index=`expr ${__lprl_index} + 1`
  done

  unset __lprl_count __lprl_index __lprl_operand __lprl_rootLogger
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>/boolean
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_doConfigure</function></funcdef>
#       <paramdef>string <parameter>configFileName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Read configuration from a file. <emphasis role="strong">The existing
#     configuration is not cleared or reset.</emphasis> If you require a
#     different behavior, then call the <code>log4sh_resetConfiguration</code>
#     before calling <code>log4sh_doConfigure</code>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_doConfigure myconfig.properties</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_doConfigure()
{
  [ -n "${FUNCNAME:-}" ] \
      && ${__LOG4SH_TRACE} "${FUNCNAME}()${BASH_LINENO:+'(called from ${BASH_LINENO})'}"

  # prepare the environment for configuration
  log4sh_resetConfiguration

  ldc_file=$1
  ldc_rtrn=${__LOG4SH_TRUE}

  # strip the config prefix and dump output to a temporary file
  ldc_tmpFile="${__log4sh_tmpDir}/properties"
  ${__LOG4SH_TRACE} "__LOG4SH_CONFIG_PREFIX='${__LOG4SH_CONFIG_PREFIX}'"
  grep "^${__LOG4SH_CONFIG_PREFIX}\." "${ldc_file}" >"${ldc_tmpFile}"

  # read the file in. using a temporary file and a file descriptor here instead
  # of piping the file into the 'while read' because the pipe causes a fork
  # under some shells which makes it impossible to get the variables passed
  # back to the parent script.
  exec 3<&0 <"${ldc_tmpFile}"
  while read ldc_line; do
    ldc_key=`expr "${ldc_line}" : '\([^= ]*\) *=.*'`
    ldc_value=`expr "${ldc_line}" : '[^= ]* *= *\(.*\)'`

    # strip the leading 'log4sh.'
    ldc_key=`_log4sh_stripPropPrefix ${ldc_key}`
    ldc_keyword=`_log4sh_getPropPrefix ${ldc_key}`
    case ${ldc_keyword} in
      alternative) _log4sh_propAlternative ${ldc_key} "${ldc_value}" ;;
      appender) _log4sh_propAppender ${ldc_key} "${ldc_value}" ;;
      logger) _log4sh_propLogger ${ldc_key} "${ldc_value}" ;;
      rootLogger) _log4sh_propRootLogger "${ldc_value}" ;;
      *)
        _log4sh_error "unrecognized properties keyword (${ldc_keyword})"
        false
        ;;
    esac
    [ $? -ne ${__LOG4SH_TRUE} ] && ldc_rtrn=${__LOG4SH_ERROR}
  done
  exec 0<&3 3<&-

  # remove the temporary file
  rm -f "${ldc_tmpFile}"

  # activate all of the appenders
  for ldc_appender in ${__log4shAppenders}; do
    ${__LOG4SH_APPENDER_FUNC_PREFIX}${ldc_appender}_activateOptions
  done

  __log4sh_return=${ldc_rtrn}
  unset ldc_appender ldc_file ldc_tmpFile ldc_line ldc_key ldc_keyword
  unset ldc_value ldc_rtrn
  return ${__log4sh_return}
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_readProperties</function></funcdef>
#       <paramdef>string <parameter>configFileName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.6</emphasis></para>
#   <para>
#     See <code>log4sh_doConfigure</code>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_readProperties myconfig.properties</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_readProperties()
{
  log4sh_doConfigure "$@"
}

#/**
# <s:function group="Property" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_resetConfiguration</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     This function completely resets the log4sh configuration to have no
#     appenders with a global logging level of ERROR.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_resetConfiguration</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
# XXX if a configuration is *repeatedly* established via logger_addAppender and
# reset using this command, there is a risk of running out of memory.
log4sh_resetConfiguration()
{
  __log4shAppenders=''
  __log4shAppenderCount=0
  __log4shAppenderCounts=''
  __log4shAppenderLayouts=''
  __log4shAppenderLevels=''
  __log4shAppenderPatterns=''
  __log4shAppenderTypes=''
  __log4shAppender_file_files=''
  __log4shAppender_rollingFile_maxBackupIndexes=''
  __log4shAppender_rollingFile_maxFileSizes=''
  __log4shAppender_smtp_tos=''
  __log4shAppender_smtp_subjects=''
  __log4shAppender_syslog_facilities=''
  __log4shAppender_syslog_hosts=''

  logger_setLevel ERROR
}

#==============================================================================
# Thread
#

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <code>string</code>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_getThreadName</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>Gets the current thread name.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>threadName=`logger_getThreadName`</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_getThreadName()
{
  echo ${__log4sh_threadName}
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_setThreadName</function></funcdef>
#       <paramdef>string <parameter>threadName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>
#     Sets the thread name (e.g. the name of the script). This thread name can
#     be used with the '%t' conversion character within a
#     <option>PatternLayout</option>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_setThreadName "myThread"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_setThreadName()
{
  _thread=$1

  _length=`_log4sh_getArrayLength "$__log4sh_threadStack"`
  __log4sh_threadStack=`_log4sh_setArrayElement "$__log4sh_threadStack" $_length $_thread`
  __log4sh_threadName=$_thread

  unset _length _thread
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_pushThreadName</function></funcdef>
#       <paramdef>string <parameter>threadName</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.7</emphasis></para>
#   <para>
#     Sets the thread name (eg. the name of the script) and pushes the old on
#     to a stack for later use. This thread name can be used with the '%t'
#     conversion character within a <option>PatternLayout</option>.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_pushThreadName "myThread"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_pushThreadName()
{
  __log4sh_threadStack=`_log4sh_pushStack "$__log4sh_threadStack" $1`
  __log4sh_threadName=$1
}

#/**
# <s:function group="Thread" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>logger_popThreadName</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para><emphasis role="strong">Deprecated as of 1.3.7</emphasis></para>
#   <para>
#     Removes the topmost thread name from the stack. The next thread name on
#     the stack is then placed in the <varname>__log4sh_threadName</varname>
#     variable. If the stack is empty, or has only one element left, then a
#     warning is given that no more thread names can be popped from the stack.
#   </para>
#   <funcsynopsis>
#     <funcsynopsisinfo>logger_popThreadName</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
logger_popThreadName()
{
  _length=`_log4sh_getArrayLength "$__log4sh_threadStack"`
  if [ $_length -gt 1 ]; then
    __log4sh_threadStack=`_log4sh_popStack "$__log4sh_threadStack"`
    __log4sh_threadName=`_log4sh_peekStack "$__log4sh_threadStack"`
  else
    echo 'log4sh:WARN no more thread names available on thread name stack.' >&2
  fi
}

#==============================================================================
# Trap
#

#/**
# <s:function group="Trap" modifier="public">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>log4sh_cleanup</function></funcdef>
#       <void />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a cleanup function to remove the temporary directory used by
#   log4sh. It is provided for scripts who want to do log4sh cleanup work
#   themselves rather than using the automated cleanup of log4sh that is
#   invoked upon a normal exit of the script.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>log4sh_cleanup</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
log4sh_cleanup()
{
  _log4sh_cleanup 'EXIT'
}

#/**
# <s:function group="Trap" modifier="private">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>_log4sh_cleanup</function></funcdef>
#       <paramdef>string <parameter>signal</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This is a cleanup function to remove the temporary directory used by
#   log4sh. It should only be called by log4sh itself when it is taking
#   control of traps.</para>
#   <para>If there was a previously defined trap for the given signal, log4sh
#   will attempt to call the original trap handler as well so as not to break
#   the parent script.</para>
#   <funcsynopsis>
#     <funcsynopsisinfo>_log4sh_cleanup EXIT</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
_log4sh_cleanup()
{
  _lc__trap=$1
  ${__LOG4SH_INFO} "_log4sh_cleanup(): the ${_lc__trap} signal was caught"

  _lc__restoreTrap=${__LOG4SH_FALSE}
  _lc__oldTrap=''

  # match trap to signal value
  case "${_lc__trap}" in
    EXIT) _lc__signal=0 ;;
    INT) _lc__signal=2 ;;
    TERM) _lc__signal=15 ;;
  esac

  # do we possibly need to restore a previous trap?
  if [ -r "${__log4sh_trapsFile}" -a -s "${__log4sh_trapsFile}" ]; then
    # yes. figure out what we need to do
    if [ `grep "^trap -- " "${__log4sh_trapsFile}" >/dev/null; echo $?` -eq 0 ]
    then
      # newer trap command
      ${__LOG4SH_DEBUG} 'newer POSIX trap command'
      _lc__restoreTrap=${__LOG4SH_TRUE}
      _lc__oldTrap=`egrep "(${_lc__trap}|${_lc__signal})$" "${__log4sh_trapsFile}" |\
        sed "s/^trap -- '\(.*\)' [A-Z]*$/\1/"`
    elif [ `grep "[0-9]*: " "${__log4sh_trapsFile}" >/dev/null; echo $?` -eq 0 ]
    then
      # older trap command
      ${__LOG4SH_DEBUG} 'older style trap command'
      _lc__restoreTrap=${__LOG4SH_TRUE}
      _lc__oldTrap=`grep "^${_lc__signal}: " "${__log4sh_trapsFile}" |\
        sed 's/^[0-9]*: //'`
    else
      # unrecognized trap output
      _log4sh_error 'unable to restore old traps! unrecognized trap command output'
    fi
  fi

  # do our work
  rm -fr "${__log4sh_tmpDir}"

  # execute the old trap
  if [ ${_lc__restoreTrap} -eq ${__LOG4SH_TRUE} -a -n "${_lc__oldTrap}" ]; then
    ${__LOG4SH_INFO} 'restoring previous trap of same type'
    eval "${_lc__oldTrap}"
  fi

  # exit for all non-EXIT signals
  if [ "${_lc__trap}" != 'EXIT' ]; then
    # disable the EXIT trap
    trap 0

    # add 127 to signal value and exit
    _lc__signal=`expr ${_lc__signal} + 127`
    exit ${_lc__signal}
  fi

  unset _lc__oldTrap _lc__signal _lc__restoreTrap _lc__trap
  return
}


#==============================================================================
# main
#

# create a temporary directory
__log4sh_tmpDir=`_log4sh_mktempDir`

# preserve old trap(s)
__log4sh_trapsFile="${__log4sh_tmpDir}/traps"
trap >"${__log4sh_trapsFile}"

# configure traps
${__LOG4SH_INFO} 'setting traps'
trap '_log4sh_cleanup EXIT' 0
trap '_log4sh_cleanup INT' 2
trap '_log4sh_cleanup TERM' 15

# alternative commands
log4sh_setAlternative mail "${LOG4SH_ALTERNATIVE_MAIL:-mail}" ${__LOG4SH_TRUE}
[ -n "${LOG4SH_ALTERNATIVE_NC:-}" ] \
    && log4sh_setAlternative nc "${LOG4SH_ALTERNATIVE_NC}"

# load the properties file
${__LOG4SH_TRACE} "__LOG4SH_CONFIGURATION='${__LOG4SH_CONFIGURATION}'"
if [ "${__LOG4SH_CONFIGURATION}" != 'none' -a -r "${__LOG4SH_CONFIGURATION}" ]
then
  ${__LOG4SH_INFO} 'configuring via properties file'
  log4sh_doConfigure "${__LOG4SH_CONFIGURATION}"
else
  if [ "${__LOG4SH_CONFIGURATION}" != 'none' ]; then
    _log4sh_warn 'No appenders could be found.'
    _log4sh_warn 'Please initalize the log4sh system properly.'
  fi
  ${__LOG4SH_INFO} 'configuring at runtime'

  # prepare the environment for configuration
  log4sh_resetConfiguration

  # note: not using the constant variables here (e.g. for ConsoleAppender) so
  # that those perusing the code can have a working example
  logger_setLevel ${__LOG4SH_LEVEL_ERROR_STR}
  logger_addAppender stdout
  appender_setType stdout ConsoleAppender
  appender_setLayout stdout PatternLayout
  appender_setPattern stdout '%-4r [%t] %-5p %c %x - %m%n'
fi

# restore the previous set of shell flags
for _log4sh_shellFlag in ${__LOG4SH_SHELL_FLAGS}; do
  echo ${__log4sh_oldShellFlags} |grep ${_log4sh_shellFlag} >/dev/null \
    || set +${_log4sh_shellFlag}
done
unset _log4sh_shellFlag

#/**
# </s:shelldoc>
#*/
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/lib/migrate�����������������������������������������0000644�0000765�0000024�00000002771�11236177363�023574� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/lib/log4sh

function mark_complete {
    migration=$1

    date >> deploy/state/$migration
    logger_info "Migration $migration ran successfully."
}

function run_migration {
    migration=$1

    logger_info "Running $migration migration."

    if sh deploy/migrations/$migration
    then
        mark_complete $migration
    else
        logger_error "Script $migration failed, exiting."
        exit 1
    fi
}

function migrate {
    TARGET=$1

    logger_info "Changing to $TARGET"

    pushd $TARGET
    assert_in $TARGET

    for migration in $(ls deploy/migrations | sort -n)
    do
        if [ -e deploy/state/$migration ]
        then
            logger_info "Skipping $migration migration."
        else
            run_migration $migration
        fi
    done

    popd
}


function backup {

    logger_info "Backing up $1 to $2 before deployment to "
    
    rdiff-backup $1 $2
    rdiff-backup -l $2
}

function rollback {
    BACK_BY=$1
    SOURCE=$2
    TARGET=$3

    logger_info "Rolling back to $BACK_BY backups ago."

    rdiff-backup -r $BACK_BY $SOURCE $TARGET
}

function deploy_code {
    logger_info "Deploying code from $1 to $2."

    rsync --exclude config -av $1 $2
}

function assert_in {
    if [ "$PWD" != "$1" ]
    then
        logger_error "You must be in the source checkout directory!"
        exit 1
    fi
}

function assert_env {
    if [ "$SOURCE" == "" ]
    then
        logger_error "You must specify an environment (testing/prod/staging)".
        exit 1
    fi
}


�������lamson-1.0pre11/examples/myinboxisnotatv/deploy/lib/shunit2�����������������������������������������0000644�0000765�0000024�00000054500�11236177363�023535� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# $Id: shunit2 71 2007-06-02 18:42:46Z sfsetse $
# vim:syntax=sh:sts=2
# vim:foldmethod=marker:foldmarker=/**,*/
#
#/**
# <?xml version="1.0" encoding="UTF-8"?>
# <s:shelldoc xmlns:s="http://www.forestent.com/projects/shelldoc/xsl/2005.0">
# <s:header>
# shUnit 2.1.0
# Shell Unit Test Framework
#
# http://shunit2.sourceforge.net/
#
# written by Kate Ward &lt;kate.ward@forestent.com&gt;
# released under the LGPL
#
# this module implements a xUnit based unit test framework similar to JUnit
# </s:header>
#*/

# shell flags for shunit:
# u - treat unset variables as an error when performing parameter expansion
__SHUNIT_SHELL_FLAGS='u'

# save the current set of shell flags, and then set some for ourselves
__shunit_oldShellFlags="$-"
for _shunit_shellFlag in `echo "${__SHUNIT_SHELL_FLAGS}" |sed 's/\(.\)/\1 /g'`
do
  set -${_shunit_shellFlag}
done

# constants

__SHUNIT_VERSION='2.1.1pre'

SHUNIT_TRUE=0
SHUNIT_FALSE=1

__SHUNIT_ASSERT_MSG_PREFIX='ASSERT:'

for _su_const in `set |grep "^__SHUNIT_" |cut -d= -f1`; do
  readonly ${_su_const}
done
unset _su_const

# variables
__shunit_skip=${SHUNIT_FALSE}
__shunit_suite=''

__shunit_testsPassed=0
__shunit_testsFailed=0
__shunit_testsSkipped=0
__shunit_testsTotal=0

#-----------------------------------------------------------------------------
# assert functions
#

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertEquals</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
assertEquals()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 3 ]; then
    _su_message=$1
    shift
  fi
  _su_expected=$1
  _su_actual=$2

  if [ "${_su_expected}" = "${_su_actual}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_expected _su_actual
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNull</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>value</emphasis> is <literal>null</literal>,
#   or in shell terms a zero-length string. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNull()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_value=$1

  _su_value2=`eval echo "${_su_value}"`
  if [ -z "${_su_value2}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_value _su_value2
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNotNull</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>value</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>value</emphasis> is <emphasis
#   role="strong">not</emphasis> <literal>null</literal>, or in shell terms not
#   a zero-length string. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNotNull()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_value=$1

  _su_value2=`eval echo "${_su_value}"`
  if [ -n "${_su_value2}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_value _su_value2
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function is functionally equivalent to
#   <function>assertEquals</function>.</para>
# </entry>
# </s:function>
#*/
assertSame()
{
  assertEquals "$@"
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertNotSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are <emphasis role="strong">not</emphasis> equal to one another. The message is optional.</para>
# </entry>
# </s:function>
#*/
assertNotSame()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 3 ]; then
    _su_message=$1
    shift
  fi
  _su_expected=$1
  _su_actual=$2

  if [ "${_su_expected}" != "${_su_actual}" ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_expected _su_actual
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertTrue</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>condition</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that a given shell test condition is true. The message is
#   optional.</para>
#   <para>Testing whether something is true or false is easy enough by using
#   the assertEquals/assertNotSame functions. Shell supports much more
#   complicated tests though, and a means to support them was needed. As such,
#   this function tests that conditions are true or false through evaluation
#   rather than just looking for a true or false.</para>
#   <funcsynopsis>
#     The following test will succeed: <funcsynopsisinfo>assertTrue "[ 34 -gt 23 ]"</funcsynopsisinfo>
#     The folloing test will fail with a message: <funcsynopsisinfo>assertTrue "test failed" "[ -r '/non/existant/file' ]"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
assertTrue()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_condition=$1

  ( eval ${_su_condition} ) >/dev/null 2>&1
  if [ $? -eq ${SHUNIT_TRUE} ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_condition
}

#/**
# <s:function group="asserts">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>assertFalse</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>condition</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Asserts that a given shell test condition is false. The message is
#   optional.</para>
#   <para>Testing whether something is true or false is easy enough by using
#   the assertEquals/assertNotSame functions. Shell supports much more
#   complicated tests though, and a means to support them was needed. As such,
#   this function tests that conditions are true or false through evaluation
#   rather than just looking for a true or false.</para>
#   <funcsynopsis>
#     The following test will succeed: <funcsynopsisinfo>assertFalse "[ 'apples' = 'oranges' ]"</funcsynopsisinfo>
#     The folloing test will fail with a message: <funcsynopsisinfo>assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]"</funcsynopsisinfo>
#   </funcsynopsis>
# </entry>
# </s:function>
#*/
assertFalse()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _su_message=''
  if [ $# -eq 2 ]; then
    _su_message=$1
    shift
  fi
  _su_condition=$1

  ( eval ${_su_condition} ) >/dev/null 2>&1
  if [ $? -eq ${SHUNIT_FALSE} ]; then
    _shunit_testPassed
    return ${SHUNIT_TRUE}
  else
    _shunit_testFailed "${_su_message}"
    return ${SHUNIT_FALSE}
  fi

  unset _su_message _su_condition
}

#-----------------------------------------------------------------------------
# failure functions
#

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>fail</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test immediately, with the optional message.</para>
# </entry>
# </s:function>
#*/
fail()
{
  _shunit_shouldSkip && return ${SHUNIT_TRUE}

  _shunit_testFailed "${@:-}"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failNotEquals</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are <emphasis role="strong">not</emphasis>
#   equal to one another. The message is optional.</para>
# </entry>
# </s:function>
#*/
failNotEquals()
{
  assertEquals "$@"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
failSame()
{
  assertNotSame "$@"
}

#/**
# <s:function group="failures">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>failNotSame</function></funcdef>
#       <paramdef>string <parameter>[message]</parameter></paramdef>
#       <paramdef>string <parameter>expected</parameter></paramdef>
#       <paramdef>string <parameter>actual</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>Fails the test if <emphasis>expected</emphasis> and
#   <emphasis>actual</emphasis> are equal to one another. The message is
#   optional.</para>
# </entry>
# </s:function>
#*/
failNotSame()
{
  assertEquals "$@"
}

#-----------------------------------------------------------------------------
# skipping functions
#

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>startSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function forces the remaining assert and fail functions to be
#   "skipped", i.e. they will have no effect. Each function skipped will be
#   recorded so that the total of asserts and fails will not be altered.</para>
# </entry>
# </s:function>
#*/
startSkipping()
{
  __shunit_skip=${SHUNIT_TRUE}
}

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>endSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function returns calls to the assert and fail functions to their
#   default behavior, i.e. they will be called.</para>
# </entry>
# </s:function>
#*/
endSkipping()
{
  __shunit_skip=${SHUNIT_FALSE}
}

#/**
# <s:function group="skipping">
# <entry align="right">
#   <emphasis>boolean</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>isSkipping</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function returns the state of skipping.</para>
# </entry>
# </s:function>
#*/
isSkipping()
{
  return ${__shunit_skip}
}

#-----------------------------------------------------------------------------
# suite functions
#

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>suite</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be optionally overridden by the user in their test
#   suite.</para>
#   <para>If this function exists, it will be called when
#   <command>shunit2</command> is sourced. If it does not exist,
#   <command>shunit2</command> will search the parent script for all functions
#   beginning with the word <literal>test</literal>, and they will be added
#   dynamically to the test suite.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# suite() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>suite_addTest</function></funcdef>
#       <paramdef>string <parameter>function</parameter></paramdef>
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function adds a function name to the list of tests scheduled for
#   execution as part of this test suite. This function should only be called
#   from within the <function>suite()</function> function.</para>
# </entry>
# </s:function>
#*/
suite_addTest()
{
  _su_func=$1

  __shunit_suite="${__shunit_suite:+${__shunit_suite} }${_su_func}"

  unset _su_func
}

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>oneTimeSetUp</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called once before any tests are
#   run. It is useful to prepare a common environment for all tests.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# oneTimeSetUp() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>oneTimeTearDown</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called once after all tests are
#   completed. It is useful to clean up the environment after all tests.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# oneTimeTearDown() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>setUp</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called before each test is run.
#   It is useful to reset the environment before each test.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# setUp() { :; }

#/**
# <s:function group="suites">
# <entry align="right">
#   <emphasis>void</emphasis>
# </entry>
# <entry>
#   <funcsynopsis>
#     <funcprototype>
#       <funcdef><function>tearDown</function></funcdef>
#       <paramdef />
#     </funcprototype>
#   </funcsynopsis>
#   <para>This function can be be optionally overridden by the user in their
#   test suite.</para>
#   <para>If this function exists, it will be called after each test completes.
#   It is useful to clean up the environment after each test.</para>
# </entry>
# </s:function>
#*/
# Note: see _shunit_mktempFunc() for actual implementation
# tearDown() { :; }

#------------------------------------------------------------------------------
# internal shUnit functions
#

_shunit_cleanup()
{
  name=$1

  case ${name} in
    EXIT) signal=0 ;;
    INT) signal=2 ;;
    TERM) signal=15 ;;
  esac

  # do our work
  rm -fr "${__shunit_tmpDir}"

  # exit for all non-EXIT signals
  if [ ${name} != 'EXIT' ]; then
    echo "trapped and now handling the ${name} signal" >&2
    _shunit_generateReport
    # disable EXIT trap
    trap 0
    # add 127 to signal and exit
    signal=`expr ${signal} + 127`
    exit ${signal}
  fi
}

_shunit_execSuite()
{
  echo '#'
  echo '# Performing tests'
  echo '#'
  for _su_func in ${__shunit_suite}; do
    # disable skipping
    endSkipping

    # execute the per-test setup function
    setUp

    # execute the test
    echo "${_su_func}"
    eval ${_su_func}

    # execute the per-test tear-down function
    tearDown
  done

  unset _su_func
}

_shunit_functionExists()
{
  _su__func=$1
  type ${_su__func} 2>/dev/null |grep "is a function$" >/dev/null
  _su__return=$?
  unset _su__func
  return ${_su__return}
}

_shunit_generateReport()
{
  _su__awk='{printf("%4d %3.0f%%", $1, $1*100/$2)}'
  if [ ${__shunit_testsTotal} -gt 0 ]; then
    _su__passed=`echo ${__shunit_testsPassed} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__failed=`echo ${__shunit_testsFailed} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__skipped=`echo ${__shunit_testsSkipped} ${__shunit_testsTotal} |\
        awk "${_su__awk}"`
    _su__total=`echo ${__shunit_testsTotal} 100 |\
        awk '{printf("%4d %3d%%", $1, $2)}'`
  else
    _su__passed=`echo 0 0 |awk '{printf("%4d %3d%%", $1, $2)}'`
    _su__failed=${_su__passed}
    _su__skipped=${_su__passed}
    _su__total=${_su__passed}
  fi

  cat <<EOF

#
# Test report
#
tests passed:  ${_su__passed}
tests failed:  ${_su__failed}
tests skipped: ${_su__skipped}
tests total:   ${_su__total}
EOF

  unset _su__awk _su__passed _su__failed _su__skipped _su__total
}

# this function is a cross-platform temporary directory creation tool. not all
# OSes have the mktemp function, so one is included here.
_shunit_mktempDir()
{
  # try the standard mktemp function
  ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ) && return

  # the standard mktemp didn't work.  doing our own.
  if [ -r '/dev/urandom' ]; then
    _su__random=`od -vAn -N4 -tx4 </dev/urandom |sed 's/^[^0-9a-f]*//'`
  elif [ -n "${RANDOM:-}" ]; then
    # $RANDOM works
    _su__random=${RANDOM}${RANDOM}${RANDOM}$$
  else
    # $RANDOM doesn't work
    _su__date=`date '+%Y%m%d%H%M%S'`
    _su__random=`expr ${_su__date} / $$`
  fi

  _su__tmpDir="${TMPDIR-/tmp}/shunit.${_su__random}"
  ( umask 077 && mkdir "${_su__tmpDir}" ) || {
    echo 'shUnit:FATAL could not create temporary directory! exiting' >&2
    exit 1
  }

  echo ${_su__tmpDir}
  unset _su__date _su__random _su__tmpDir
}

# this function is here to work around issues in Cygwin
_shunit_mktempFunc()
{
  for _su__func in oneTimeSetUp oneTimeTearDown setUp tearDown suite; do
    _su__file="${__shunit_tmpDir}/${_su__func}"
    cat <<EOF >"${_su__file}"
#! /bin/sh
exit 0
EOF
    chmod +x "${_su__file}"
  done

  unset _su__file
}

_shunit_shouldSkip()
{
  [ ${__shunit_skip} -eq ${SHUNIT_FALSE} ] && return ${SHUNIT_FALSE}
  _shunit_testSkipped
}

_shunit_testPassed()
{
  __shunit_testsPassed=`expr ${__shunit_testsPassed} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
}

_shunit_testFailed()
{
  [ $# -eq 1 ] && _su__msg=$1

  __shunit_testsFailed=`expr ${__shunit_testsFailed} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`

  [ -z "${_su__msg:-}" ] && _su__msg='failed'
  echo "${__SHUNIT_ASSERT_MSG_PREFIX} ${_su__msg}" >&2
  unset _su__msg
}

_shunit_testSkipped()
{
  __shunit_testsSkipped=`expr ${__shunit_testsSkipped} + 1`
  __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
}

#------------------------------------------------------------------------------
# main
#

# create a temporary storage location
__shunit_tmpDir=`_shunit_mktempDir`

# setup traps to clean up after ourselves
trap '_shunit_cleanup EXIT' 0
trap '_shunit_cleanup INT' 2
trap '_shunit_cleanup TERM' 15

# create phantom functions to work around issues with Cygwin
_shunit_mktempFunc
PATH="${__shunit_tmpDir}:${PATH}"

# execute the oneTimeSetUp function (if it exists)
#_shunit_functionExists oneTimeSetUp && oneTimeSetUp
oneTimeSetUp

# deprecated: execute the suite function defined in the parent test script
suite

# if no suite function was defined, dynamically build a list of functions
if [ -z "${__shunit_suite}" ]; then
  funcs=`grep "^[ \t]*test[A-Za-z0-9_]* *()" $0 |sed 's/[^A-Za-z0-9_]//g'`
  for func in ${funcs}; do
    suite_addTest ${func}
  done
fi

# execute the tests
_shunit_execSuite

# execute the oneTimeTearDown function (if it exists)
oneTimeTearDown

# generate report
_shunit_generateReport

# restore the previous set of shell flags
for _shunit_shellFlag in ${__SHUNIT_SHELL_FLAGS}; do
  echo ${__shunit_oldShellFlags} |grep ${_shunit_shellFlag} >/dev/null \
    || set +${_shunit_shellFlag}
done
unset _shunit_shellFlag

#/**
# </s:shelldoc>
#*/
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/log4sh.properties�����������������������������������0000644�0000765�0000024�00000000455�11236177363�024766� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Set root logger level to INFO and its only appender to A1
log4sh.rootLogger=INFO, A1

# A1 is set to be a ConsoleAppender.
log4sh.appender.A1=ConsoleAppender

# A1 uses a PatternLayout.
log4sh.appender.A1.layout=PatternLayout
log4sh.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/rollback��������������������������������������������0000644�0000765�0000024�00000000516�11236177363�023162� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������source deploy/env/$1
source deploy/lib/migrate

assert_env
setup
stop $TARGET

TS=`date +%F.%T`

mv $TARGET $FAILS/$TS
logger_info "Failure was placed in $TS, if the backup fails go there."

rollback 0B $BACKUP $TARGET

logger_info "Rollback complete and failure placed in $TS"
logger_warn "System is now stopped, you should fix it."
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/scripts/��������������������������������������������0000755�0000765�0000024�00000000000�11313464574�023132� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/deploy/scripts/json_convert.py�����������������������������0000644�0000765�0000024�00000001442�11236177363�026217� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys
sys.path.append(".")

from lamson.mail import MailRequest, MailResponse
from lamson.queue import Queue
import config.testing
from app.model import archive
import os


def convert_queue(arg, dirname, names):
    if dirname.endswith("new"):
        print dirname, names

        jpath = dirname + "/../../json"
        if not os.path.exists(jpath):
            os.mkdir(jpath)

        for key in names:
            json_file = key + ".json"
            json_archive = os.path.join(jpath, json_file)

            fpath = os.path.join(dirname, key)
            msg = MailRequest('librelist.com', None, None, open(fpath).read())
            f = open(json_archive, "w")
            f.write(archive.to_json(msg.base))
            f.close()

os.path.walk("app/data/archives", convert_queue, None)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/muttrc�����������������������������������������������������0000644�0000765�0000024�00000000345�11236177155�021412� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/README�����������������������������������������������������0000644�0000765�0000024�00000000201�11236177512�021015� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������This is the code for the myinboxisnota.tv website.  It is given as an example of using Lamson as a
anonymizer and filter system.
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/�����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021311� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/handlers/��������������������������������������������0000755�0000765�0000024�00000000000�11313464574�023111� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/handlers/__init__.py���������������������������������0000644�0000765�0000024�00000000000�11236177155�025210� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/handlers/anonymizer_tests.py�������������������������0000644�0000765�0000024�00000002634�11251313026�027067� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson import mail
from config import settings
from app.model import addressing


client = RouterConversation("person@localhost", "Anonymizing Tests")
marketroid = RouterConversation("buymycrap@localhost", "Marketroid Tests")
host = "myinboxisnota.tv"

def setup():
    client.begin()
    marketroid.begin()

def teardown():
    addressing.delete("person@localhost")
    addressing.delete("buymycrap@localhost")


def test_client_subscribes():
    client.begin()
    confirm = client.say("start@%s" % host, "subscribe me", "start-confirm")
    welcome = client.say(confirm['from'], "confirm me", "user")

    return welcome['from']

def test_client_receives_normal_mail():
    marketroid.begin()
    user_id = test_client_subscribes()
    
    to_user = marketroid.say(user_id, "I have a great offer for you!", "marketroid")

    assert to_user['reply-to'] != "buymycrap@localhost"
    to_marketroid = client.say(to_user['reply-to'], "I don't want your junk.", "user")
   
    assert to_marketroid['from'] != 'person@localhost'

    to_user2 = marketroid.say(to_marketroid['from'], "Hey you should buy my stuff.", "marketroid")

    assert_equal(to_user['reply-to'], to_user2['reply-to'])

    addressing.delete(user_id.split('@')[0])


def test_user_to_user_forbid():
    user_id = test_client_subscribes()

    client.say(user_id, "I want to email myself.", "noreply")
    

����������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/index.html�������������������������������������������0000644�0000765�0000024�00000006655�11236221166�023312� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<html>

<head>

<meta name="Description" content="Information architecture, Web Design, Web Standards." />
<meta name="Keywords" content="inbox, tv, html, cleaning" />
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta name="Distribution" content="Global" />
<meta name="Robots" content="index,follow" />		

<link rel="stylesheet" href="images/style.css" type="text/css" />

<title>My Inbox Is Not A TV</title>
	
</head>

<body>

	<!-- navigation starts here -->
	<div id="nav">
		
		<ul>
			<li id="current"><a href="index.html">Home</a></li>
		</ul>
	
	</div>

	<!-- header starts here -->
	<div id="header">	
	
		<div id="clouds"></div>
		
		<h1 id="logo-text"><a href="index.html" title="">My Inbox Is Not A TV</a></h1>	
		<p id="slogan">Putting an end to HTML Email marketing.</p>				
	
	</div>	
				
	<!-- content-wrap starts here -->
	<div id="content-wrap"><div id="content">	 
	
		<div id="sidebar" >	
		
			<h3>Menu</h3>			
			<ul class="sidemenu">
				<li><a href="index.html">Home</a></li>
			</ul>	
				
				
            <h3>About MyInboxIsNotA.TV</h3>
            <p>
            MyInboxIsNotA.TV is an email service that let's you subscribe to
            marketing newsletters that you're forced to accept, but filter
            them for HTML, web bugs, spam, and other things you don't want.

            It is another demonstration of the <a href="http://lamsonproject.org">Lamson Project</a>
            and is free for now.
            </p>	
		
		</div>	
	
        <div id="main">
            <h2>Strip, Cleanse, And Read</h2>

            <p>
            The sad thing is, marketing people think about your Inbox like it's
            a TV.  They desperately want to send you their HTML marketing material
            so you are forced to view it.  They believe that the fraction of a second
            you spend viewing their 1990's styled HTML marketing will convert you
            to a paying customer.
            Successful companies know this isn't a long-term way to develop a 
            customer base, but other companies need help figuring this out.
            </p>


            <p>
            MyInboxIsNotA.TV is a simple service to let you subscribe to any corporate
            mailing list or service without receiving their HTML spam.  You give
            them a proxy address from MyInboxIsNotA.TV and it will filter any HTML leaving
            you with just nice clean text to read.  We'll block spam, stop web
            bugs, and even flag their stuff with a special header so you can 
            filter it further.
            </p>

            <p>Stay tuned for the launch in the next few days.</p>
				
			<br />
		</div>					
		
	<!-- content-wrap ends here -->		
	</div></div>

	<!-- footer starts here-->	
	<div id="footer-wrap">
	
		<!-- columns starts here-->		
		<div id="columns">
	
		<!-- columns ends -->
		</div>	
	
		<div id="footer-bottom">		
			<p>
			2009 <strong>Zed A. Shaw</strong> | 
			Design by: <a href="http://www.styleshout.com/">styleshout</a> | 
			Valid <a href="http://validator.w3.org/check?uri=referer">XHTML</a> | 
			<a href="http://jigsaw.w3.org/css-validator/check/referer">CSS</a>
			
			<a href="index.html">Home</a>
   		<a href="index.html">Sitemap</a>
	  		<a href="index.html">RSS Feed</a>
			</p>		
		</div>	
	
	<!-- footer ends-->		
	</div>

</body>
</html>
�����������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/model/�����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022411� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/model/__init__.py������������������������������������0000644�0000765�0000024�00000000000�11236177155�024510� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/model/addressing_tests.py����������������������������0000644�0000765�0000024�00000003256�11237413760�026332� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson import mail
from config import settings
from app.model import addressing

user_real = 'zedshaw@zedshaw.com'
user_id = "user-%s" % addressing.random_id()
host = 'myinboxisnota.tv'

def test_store_lookup_delete():
    addressing.store(user_id, user_real)
    addr = addressing.lookup(user_id)
    assert_equal(addr, user_real)
   
    addressing.delete(user_id)
    assert_raises(KeyError, addressing.lookup, user_id)


def test_store_lookup_delete_with_dumb_addresses():
    addressing.store('"Zed Shaw" <zedshaw@zedshaw.com>', "fake")
    assert_equal("fake", addressing.lookup("zedshaw@zedshaw.com"))
    assert_equal("fake", addressing.lookup('"Zed Shaw" <zedshaw@zedshaw.com>'))
    addressing.delete("zedshaw@zedshaw.com")
    assert_raises(KeyError, addressing.lookup, "zedshaw@zedshaw.com")
    assert_raises(KeyError, addressing.lookup,'"Zed Shaw" <zedshaw@zedshaw.com>')

def test_random_id():
    id_number = addressing.random_id()
    assert id_number

def test_real():
    addressing.store(user_id, user_real)
    assert_equal(addressing.real(user_id), user_real)
    addressing.delete(user_id)


def test_anon():
    addressing.store(user_real, user_id)

    user_anon = addressing.anon(user_real, host)
    assert_equal(user_anon, user_id + '@' + host)

    addressing.delete(user_real)

def test_mapping():
    anon = addressing.mapping(user_real, 'user', host)
    anon_id = anon.split('@')[0]

    assert_equal(addressing.lookup(anon_id), user_real)
    assert_equal(addressing.lookup(user_real), anon_id)
    assert_equal(addressing.lookup(anon_id), user_real)

    addressing.delete(anon_id)
    addressing.delete(user_real)

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/model/filter_tests.py��������������������������������0000644�0000765�0000024�00000005254�11237401362�025467� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson import mail
from config import settings
from app.model import filter, addressing


host = 'myinboxisnota.tv'
user = 'joe@leavemealone.com'
user_anon_addr = addressing.mapping(user, 'user', host)
marketroid = 'marketroid@buymycrap.com'
mk_anon_addr = addressing.mapping(marketroid, 'marketroid', host)
user_id = user_anon_addr.split('@')[0]
marketroid_id = mk_anon_addr.split('@')[0]


from_marketroid = mail.MailResponse(From=marketroid, To=user_anon_addr, Subject="Buy my crap!",
                                    Html="<html></body>You should buy this!</body></html>")
from_user = mail.MailResponse(From=user, To=mk_anon_addr, Subject="No thanks.",
                              Body="Sorry but I'd rather not.")


def setup():
    addressing.store(user_id, user)
    addressing.store(marketroid_id, marketroid)
    addressing.store(marketroid, marketroid_id)

def teardown():
    addressing.delete(user_id)
    addressing.delete(marketroid_id)
    addressing.delete(marketroid)


def test_craft_response():
    # message from marketroid to the user_anon_addr
    msg = mail.MailRequest('fakepeer', from_marketroid['from'],
                           from_marketroid['to'], str(from_marketroid))
    
    # the mail a user would need to respond to
    resp = filter.craft_response(msg, msg['From'], user,
                                 mk_anon_addr).to_message()

    assert_equal(resp['from'], marketroid)
    assert_equal(resp['to'], user)
    assert_equal(resp['reply-to'], mk_anon_addr)

    msg = mail.MailRequest('fakepeer', from_user['from'], from_user['to'],
                           str(from_user))

    # the mail a marketroid could respond to
    resp = filter.craft_response(msg, user_anon_addr, marketroid).to_message()
    assert_equal(resp['from'], user_anon_addr)
    assert_equal(resp['to'], marketroid)

    # make sure the user's address is never in a header
    for k,v in resp.items():
        assert_not_equal(resp[k], user)


def test_cleanse_incoming():
    msg = mail.MailRequest('fakepeer', from_marketroid['from'],
                           from_marketroid['to'], str(from_marketroid))

    reply = filter.cleanse_incoming(msg, user_id, host).to_message()
    assert_equal(reply['from'], marketroid)
    assert_equal(reply['to'], user)
    assert_equal(reply['reply-to'], mk_anon_addr)


def test_route_reply():
    msg = mail.MailRequest('fakepeer', from_user['from'], from_user['to'],
                           str(from_user))
    reply = filter.route_reply(msg, marketroid_id, host).to_message()

    # make sure the user's address is never in a header
    for k,v in reply.items():
        assert_not_equal(reply[k], user)


����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/model/html_tests.py����������������������������������0000644�0000765�0000024�00000001102�11236224710�025131� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from app.model import html



def test_strip_html():
    doc = """<html><body>
        <h1>Title 1</h1>
        <p>Hello there.</p>
        <p>I like your shirt.</p>
        <p><a href="http://myinboxisnota.tv">Go here for help.</a></p>
        </body></html>
        """
    txt = html.strip_html(doc)

    assert txt
    assert_not_equal(txt, html)
    assert "<" not in txt


def test_strip_big_html():
    doc = open("tests/index.html").read()
    txt = html.strip_html(doc)
    assert txt
    assert_not_equal(txt, html)
    assert "<" not in txt

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/templates/�������������������������������������������0000755�0000765�0000024�00000000000�11313464574�023307� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/myinboxisnotatv/tests/templates/__init__.py��������������������������������0000644�0000765�0000024�00000000000�11236177155�025406� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/�����������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�015455� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/�������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016235� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/__init__.py��������������������������������������������������������0000644�0000765�0000024�00000000000�11207053442�020322� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/��������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017146� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/about.html����������������������������������������������������0000644�0000765�0000024�00000003000�11257722315�021134� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

        <title>OneShotBlog(TM)</title>
        <link rel="stylesheet" type="text/css" href="styles/reset.css" />
        <link rel="stylesheet" type="text/css" href="styles/main.css" />
    </head>

    <body>
        <div class="wrapper">
            <div class="header">
                <h1><a href="/index.html">One. Sh<span>o</span>t. Blog.</a></h1>
                <ul>
                    <li><a href="/index.html">latest</a></li>
                    <li><a href="/about.html">about</a></li>
                    <li><a href="/help.html">help</a></li>
                </ul>
            </div>
            <ul class="notifications">
                <li class="footer">
                <div>
                    <p>Software Copyright (C) 2009 Zed Shaw. Posts Copyright by their owners, sue them.</p>
                </div>
                </li>
            </ul>
            <div class="content">
                <div class="post">
                    <h2>About OneShotBlog</h2>
                    <p>OneShotBlog(TM) is a sample application for the <a href="http://lamsonproject.org">Lamson Project(TM)</a>
                    and is in alpha state right now.  Use at your own risk.</p>
                </div>
            </div>
        </div>
    </body>
</html>
lamson-1.0pre11/examples/osb/app/data/help.html�����������������������������������������������������0000644�0000765�0000024�00000006237�11257722315�020771� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

        <title>One Shot Blog: Zed A. Shaw</title>
        <link rel="stylesheet" type="text/css" href="styles/reset.css" />
        <link rel="stylesheet" type="text/css" href="styles/main.css" />
    </head>

    <body>
        <div class="wrapper">
            <div class="header">
                <h1><a href="/index.html">One. Sh<span>o</span>t. Blog.</a></h1>
                <ul>
                    <li><a href="/index.html">latest</a></li>
                    <li><a href="/about.html">about</a></li>
                </ul>
            </div>
            <ul class="notifications">
                <li class="footer">
                <div>
                    <p>Software Copyright (C) 2009 Zed Shaw. Posts Copyright by their owners, sue them.</p>
                </div>
                </li>
            </ul>
            <div class="content">

                <div class="post">
                    <h2>OneShotBlog Help</h2>

                    <p>OneShotBlog(TM) takes your blog post from your email.  That's pretty much it.  The email address you use @oneshotblog.com can 
                    be any tag you want, which will be used as the file name for your page.  Don't worry though, we won't let you pick some stupid name like ../../../../etc/passwd.  When you send your email, you get a confirmation, then your page is posted.  That's it.  It can be anything that starts with a character or number, and has characters, numbers, or periods in it.
                    </p>

                    <p>A tag like i.want.my.blog@oneshotblog.com works, a tag like ohreally?@oneshotblog.com does not.</p>

                    <p>It's not a blog in the traditional sense though, since your post is just put onto the main <a href="/index.html">index</a>.  Instead
                    it's for a single shot page you want to share, kind of like a paste-bin service if you will.  That'll probably change in the future, but
                    for now, you send a post, it gets created, and it's put in the index.</p>

                    <p>Since most people's first post is something lame like "testing" we don't index that one.  You second one after you are confirmed 
                    is the real one.  That first one is to just make sure your gear works.
                    </p>

                    <p>You can delete your posts too, and the email you get tells you how.  However, if you delete a post, it's still shown in the
                    index for now.  Sorry, this is alpha software my friends.</p>

                    <p>This server will be temporary, and will be taken down periodically.  You'll be notified when it's made official.</p>

                    <p>Finally, this whole thing is written using a tiny amount of <a href="http://lamsonproject.org">Lamson</a> code.
                    Check the project out if you want to do cool things with email.</p>

                </div>
            </div>
        </div>
    </body>
</html>
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/jquery.js�����������������������������������������������������0000644�0000765�0000024�00000157646�11257722315�021043� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*
 * jQuery JavaScript Library v1.3.2
 * http://jquery.com/
 *
 * Copyright (c) 2009 John Resig
 * Dual licensed under the MIT and GPL licenses.
 * http://docs.jquery.com/License
 *
 * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
 * Revision: 6246
 */
(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),this.length>1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return J};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y}return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return s.call(E)==="[object Array]"},isXMLDoc:function(E){return E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){if(G&&/\S/.test(G)){var F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var E,H=0,I=G.length;if(F){if(I===g){for(E in G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(I)){I=I.call(H,F)}return typeof I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?" ":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+"></"+T+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/<tbody/i.test(S),N=!O.indexOf("<table")&&!R?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return -1},merge:function(H,E){var F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return F.concat.apply([],F)}});var C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|webkit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return o.dir(E,"previousSibling")},siblings:function(E){return o.sibling(E.parentNode.firstChild,E)},children:function(E){return o.sibling(E.firstChild)},contents:function(E){return o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(G){var J=[],L=o(G);for(var K=0,H=L.length;K<H;K++){var I=(K>0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
/*
 * Sizzle CSS Selector Engine - v0.9.3
 *  Copyright 2009, The Dojo Foundation
 *  Released under the MIT, BSD, and GPL Licenses.
 *  More information: http://sizzlejs.com/
 */
(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa<ab.length;aa++){if(ab[aa]===ab[aa-1]){ab.splice(aa--,1)}}}}}return ab};F.matches=function(T,U){return F(T,null,null,U)};F.find=function(aa,T,ab){var Z,X;if(!aa){return[]}for(var W=0,V=I.order.length;W<V;W++){var Y=I.order[W],X;if((X=I.match[Y].exec(aa))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){X[1]=(X[1]||"").replace(/\\/g,"");Z=I.find[Y](X,T,ab);if(Z!=null){aa=aa.replace(I.match[Y],"");break}}}}if(!Z){Z=T.getElementsByTagName("*")}return{set:Z,expr:aa}};F.filter=function(ad,ac,ag,W){var V=ad,ai=[],aa=ac,Y,T,Z=ac&&ac[0]&&Q(ac[0]);while(ad&&ac.length){for(var ab in I.filter){if((Y=I.match[ab].exec(ad))!=null){var U=I.filter[ab],ah,af;T=false;if(aa==ai){ai=[]}if(I.preFilter[ab]){Y=I.preFilter[ab](Y,aa,ag,ai,W,Z);if(!Y){T=ah=true}else{if(Y===true){continue}}}if(Y){for(var X=0;(af=aa[X])!=null;X++){if(af){ah=U(af,Y,X,aa);var ae=W^!!ah;if(ag&&ah!=null){if(ae){T=true}else{aa[X]=false}}else{if(ae){ai.push(af);T=true}}}}}if(ah!==g){if(!ag){aa=ai}ad=ad.replace(I.match[ab],"");if(!T){return[]}break}}}if(ad==V){if(T==null){throw"Syntax error, unrecognized expression: "+ad}else{break}}V=ad}return aa};var I=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(T){return T.getAttribute("href")}},relative:{"+":function(aa,T,Z){var X=typeof T==="string",ab=X&&!/\W/.test(T),Y=X&&!ab;if(ab&&!Z){T=T.toUpperCase()}for(var W=0,V=aa.length,U;W<V;W++){if((U=aa[W])){while((U=U.previousSibling)&&U.nodeType!==1){}aa[W]=Y||U&&U.nodeName===T?U||false:U===T}}if(Y){F.filter(T,aa,true)}},">":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){var W=Y.parentNode;Z[V]=W.nodeName===U?W:false}}}else{for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){Z[V]=X?Y.parentNode:Y.parentNode===U}}if(X){F.filter(U,Z,true)}}},"":function(W,U,Y){var V=L++,T=S;if(!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("parentNode",U,V,W,X,Y)},"~":function(W,U,Y){var V=L++,T=S;if(typeof U==="string"&&!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("previousSibling",U,V,W,X,Y)}},find:{ID:function(U,V,W){if(typeof V.getElementById!=="undefined"&&!W){var T=V.getElementById(U[1]);return T?[T]:[]}},NAME:function(V,Y,Z){if(typeof Y.getElementsByName!=="undefined"){var U=[],X=Y.getElementsByName(V[1]);for(var W=0,T=X.length;W<T;W++){if(X[W].getAttribute("name")===V[1]){U.push(X[W])}}return U.length===0?null:U}},TAG:function(T,U){return U.getElementsByTagName(T[1])}},preFilter:{CLASS:function(W,U,V,T,Z,aa){W=" "+W[1].replace(/\\/g,"")+" ";if(aa){return W}for(var X=0,Y;(Y=U[X])!=null;X++){if(Y){if(Z^(Y.className&&(" "+Y.className+" ").indexOf(W)>=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return U<T[3]-0},gt:function(V,U,T){return U>T[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W<T;W++){if(Y[W]===Z){return false}}return true}}}},CHILD:function(T,W){var Z=W[1],U=T;switch(Z){case"only":case"first":while(U=U.previousSibling){if(U.nodeType===1){return false}}if(Z=="first"){return true}U=T;case"last":while(U=U.nextSibling){if(U.nodeType===1){return false}}return true;case"nth":var V=W[2],ac=W[3];if(V==1&&ac==0){return true}var Y=W[0],ab=T.parentNode;if(ab&&(ab.sizcache!==Y||!T.nodeIndex)){var X=0;for(U=ab.firstChild;U;U=U.nextSibling){if(U.nodeType===1){U.nodeIndex=++X}}ab.sizcache=Y}var aa=T.nodeIndex-ac;if(V==0){return aa==0}else{return(aa%V==0&&aa/V>=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V<T;V++){U.push(X[V])}}else{for(var V=0;X[V];V++){U.push(X[V])}}}return U}}var G;if(document.documentElement.compareDocumentPosition){G=function(U,T){var V=U.compareDocumentPosition(T)&4?-1:U===T?0:1;if(V===0){hasDuplicate=true}return V}}else{if("sourceIndex" in document.documentElement){G=function(U,T){var V=U.sourceIndex-T.sourceIndex;if(V===0){hasDuplicate=true}return V}}else{if(document.createRange){G=function(W,U){var V=W.ownerDocument.createRange(),T=U.ownerDocument.createRange();V.selectNode(W);V.collapse(true);T.selectNode(U);T.collapse(true);var X=V.compareBoundaryPoints(Range.START_TO_END,T);if(X===0){hasDuplicate=true}return X}}}}(function(){var U=document.createElement("form"),V="script"+(new Date).getTime();U.innerHTML="<input name='"+V+"'/>";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="<a href='#'></a>";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="<p class='TEST'></p>";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="<div class='test e'></div><div class='test'></div>";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1&&!ac){T.sizcache=Y;T.sizset=W}if(T.nodeName===Z){X=T;break}T=T[U]}ad[W]=X}}}function S(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1){if(!ac){T.sizcache=Y;T.sizset=W}if(typeof Z!=="string"){if(T===Z){X=true;break}}else{if(F.filter(Z,[T]).length>0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z<U;Z++){F(T,V[Z],W)}return F.filter(X,W)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=function(T){return T.offsetWidth===0||T.offsetHeight===0};F.selectors.filters.visible=function(T){return T.offsetWidth>0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return this},live:function(G,F){var E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return this}});function c(H){var E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});F.sort(function(J,I){return o.data(J.elem,"closest")-o.data(I.elem,"closest")});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){return(G=false)}});return G}function i(F,E){return["live",F,E.replace(/\./g,"`").replace(/ /g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var x=false;function B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&l==l.top){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new Date).getTime();K.style.display="none";K.innerHTML='   <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width=L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L).style.display="none"})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H<F;H++){var E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" />").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H<F;H++){this[H].style.display=o.data(this[H],"olddisplay")||""}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}}for(var G=0,F=this.length;G<F;G++){this[G].style.display="none"}return this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return this[E.queue===false?"each":"queue"](function(){var K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n);n=g}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})();������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/spam.html�����������������������������������������������������0000644�0000765�0000024�00000003166�11257722315�020777� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

        <title>OneShotBlog(TM)</title>
        <link rel="stylesheet" type="text/css" href="styles/reset.css" />
        <link rel="stylesheet" type="text/css" href="styles/main.css" />
    </head>

    <body>
        <div class="wrapper">
            <div class="header">
                <h1><a href="/index.html">One. Sh<span>o</span>t. Blog.</a></h1>
                <ul>
                    <li><a href="/index.html">latest</a></li>
                    <li><a href="/about.html">about</a></li>
                    <li><a href="/help.html">help</a></li>
                </ul>
            </div>
            <ul class="notifications">
                <li class="footer">
                <div>
                    <p>Software Copyright (C) 2009 Zed Shaw. Posts Copyright by their owners, sue them.</p>
                </div>
                </li>
            </ul>
            <div class="content">
                <div class="post">
                    <h2>SPAM SPAM SPAM spam spam...</h2>
                    <p>Thanks for marking that as spam.  Use your back button technology to continue with your day.</p>

                    <p>It won't go away for a little while, but unlike craigslist, we'll actually
                    train a real spam database and prevent future spam submissions like this.</p>
                </div>
            </div>
        </div>
    </body>
</html>
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/styles/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020471� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/data/styles/main.css�����������������������������������������������0000644�0000765�0000024�00000004717�11257722315�022135� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������div.wrapper {
    font-family:Helvetica,Arial,sans-serif;
    left:100px;
    margin:0 auto;
    padding-top:20px;
    position:relative;
    width:770px;
}
h1 a {
    color:#222222;
    float:left;
    font-size:60px;
    margin-top: 40px;
    font-weight:bold;
    letter-spacing:-2px;
    padding:5px 5px 5px 0;
    text-decoration:none;
    text-transform:uppercase;
    width:600px;
    word-spacing:5px;
}
h1 span {
    color:#1C6191;
    font-weight:bold;
}
div.header {
    height:150px;
}
div.header ul {
    float:right;
}
div.header ul li {
    background-color:#222222;
    margin-bottom:5px;
    text-align:right;
}
div.header ul li a {
    color:#EEEEEE;
    display:block;
    font-size:30px;
    padding:5px;
    text-decoration:none;
}
div.header ul li a:hover {
    background-color:#FFFFFF;
    color:#1C6191;
}
ul.notifications {
    left:-200px;
    position:absolute;
    width:190px;
    z-index:10;
}
ul.notifications li {
    background-color:#222222;
    color:#AAAAAA;
    margin-bottom:5px;
    overflow: hidden;
    padding:5px;
}

ul.notifications li div h3 {
    color:#EEEEEE;
    margin-bottom:10px;
}
ul.notifications li div p {
    padding-left:10px;
    padding-right:10px;
}
ul.notifications li div p {
    font-size:13px;
    line-height:14px;
}
ul.notifications li div p a {
    color:#FFFFFF;
    font-weight:bold;
    text-decoration:none;
}
ul.notifications li div p a:hover {
    text-decoration:underline;
}
ul.notifications li.footer div p {
    color:#EEEEEE;
    font-size:14px;
    line-height:17px;
}
div.content {
    clear:both;
}
div.post {
    border-top:5px solid #EEEEEE;
    padding-bottom:20px;
    padding-top:10px;
}
div.post p a {
    color:#000000;
    font-weight:bold;
    text-decoration:none;
}
div.post p a:hover {
    text-decoration:underline;
}
div.post pre.code {
    background-color:#EEEEEE;
    display:block;
    font-family:"Courier New",Courier,mono;
    font-size:14px;
    line-height:17px;
    margin:15px;
    overflow:auto;
    padding:5px;
    position:inherit;
    width:700px;
}
div.post h2 {
    display:block;
    font-size:35px;
    letter-spacing:1px;
    text-decoration:none;
    text-transform:lowercase;
}
div.post h2 a {
    color:#1C6191;
    display:block;
    font-size:35px;
    letter-spacing:1px;
    text-decoration:none;
    text-transform:lowercase;
}
div.post h2 a:hover {
    background-color:#222222;
    color:#EEEEEE;
}
div.post p {
    font-size:14px;
    line-height:18px;
    padding:15px 25px 5px;
}
�������������������������������������������������lamson-1.0pre11/examples/osb/app/data/styles/reset.css����������������������������������������������0000644�0000765�0000024�00000001125�11257722315�022321� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������html,body,div,span,
applet,object,iframe,
h1,h2,h3,h4,h5,h6,p,blockquote,pre,
a,abbr,acronym,address,big,cite,code,
del,dfn,em,font,img,ins,kbd,q,s,samp,
small,strike,strong,sub,sup,tt,var,
dd,dl,dt,li,ol,ul,
fieldset,form,label,legend,
table,caption,tbody,tfoot,thead,tr,th,td {
	margin: 0;
	padding: 0;
	border: 0;
	font-weight: normal;
	font-style: normal;
	font-size: 100%;
	line-height: 1;
	font-family: inherit;
	text-align: left;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}
ol,ul {
	list-style: none;
}
q:before,q:after,
blockquote:before,blockquote:after {
	content: "";
}
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/handlers/����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020035� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/handlers/__init__.py�����������������������������������������������0000644�0000765�0000024�00000000000�11207053442�022122� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/handlers/comment.py������������������������������������������������0000644�0000765�0000024�00000003706�11242131452�022042� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from app.model import post, comment
from email.utils import parseaddr
from config.settings import relay, SPAM, CONFIRM
from lamson import view, queue
from lamson.routing import route, stateless
from lamson.spam import spam_filter
import logging



@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
def SPAMMING(message, **options):
    return SPAMMING


@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
@spam_filter(SPAM['db'], SPAM['rc'], SPAM['queue'], next_state=SPAMMING)
def START(message, user_id=None, post_name=None, host=None, domain=None):
    comment.attach_headers(message, user_id, post_name, domain) 
    CONFIRM.send(relay, "comment", message, "mail/comment_confirm.msg", locals())
    return CONFIRMING


@route("comment-confirm-(id_number)@(host)", id_number="[a-z0-9]+")
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('comment', message['from'], id_number)

    if original:
        # headers are already attached from START
        comment.defer_to_queue(original)
        msg = view.respond(locals(), "mail/comment_submitted.msg",
                           From="noreply@%(host)s",
                           To=original['from'],
                           Subject="Your comment has been posted.")

        relay.deliver(msg)

        return COMMENTING
    else:
        logging.debug("Invalid confirm from %s", message['from'])
        return CONFIRMING


@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
def COMMENTING(message, user_id=None, post_name=None, host=None, domain=None):
    comment.attach_headers(message, user_id, post_name, domain) 
    comment.defer_to_queue(message)
    original = message # keeps the template happy

    msg = view.respond(locals(), "mail/comment_submitted.msg",
                       From="noreply@%(host)s",
                       To=original['from'],
                       Subject="Your comment has been posted.")
    relay.deliver(msg)

    return COMMENTING



����������������������������������������������������������lamson-1.0pre11/examples/osb/app/handlers/index.py��������������������������������������������������0000644�0000765�0000024�00000004041�11221064147�021503� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import with_statement
from email.utils import parseaddr
from lamson import view, queue
from lamson.routing import route, stateless
import logging
from config import settings
from app.model import post
from markdown import markdown



@route("(post_name)@(host)")
@stateless
def POSTING(message, post_name=None, host=None):
    user, address = parseaddr(message['from'])
    user = user or address
    post_url = "posts/%s/%s.html" % (address, post_name)

    index_q = queue.Queue("run/indexed")
    post_keys = sorted(index_q.keys(), reverse=True)
    old_keys = post_keys[50:]
    del post_keys[50:]

    # find the old one and remove it
    posts = []
    for key in post_keys:
        msg = index_q.get(key)
        if msg['x-post-url'] == post_url:
            # this is the old one, take it out
            index_q.remove(key)
        else:
            posts.append(msg)

    # update the index and our posts
    message['X-Post-URL'] = post_url
    index_q.push(message)
    posts.insert(0, message)

    # and generate the index with what we got now
    index = view.render(locals(), "web/index.html")

    f = open("app/data/index.html", "w")
    f.write(index.encode("utf-8"))
    f.close()

    # finally, zap all the old keys
    for old in old_keys: index_q.remove(old)


@route("(user_id)-AT-(domain)-(post_name)-comment@(host)")
@stateless
def COMMENTING(message, user_id=None, domain=None, post_name=None, host=None):
    address = user_id + '@' + domain
    user_dir = post.get_user_dir(address)

    if post.user_exists(address):
        # stuff it here for now, but we'll just build the file rolling
        comments = queue.Queue("%s/comments" % user_dir)
        comments.push(message)
        
        contents = markdown(message.body())
        comment_file = "%s/%s-comments.html" % (user_dir, post_name)
        snippet = view.render(locals(), "web/comments.html")
        with open(comment_file, "a") as out:
            out.write(snippet)

    else:
        logging.warning("Attempt to post to user %r but user doesn't exist.", address)

�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/handlers/post.py���������������������������������������������������0000644�0000765�0000024�00000004252�11242131470�021362� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from app.model import post
from email.utils import parseaddr
from config.settings import relay, CONFIRM
from lamson import view, queue
from lamson.routing import route, stateless
import logging


@route("(post_name)@(host)")
def START(message, post_name=None, host=None):
    message['X-Post-Name'] = post_name

    CONFIRM.send(relay, "post", message, "mail/confirm.msg", locals())
    return CONFIRMING


@route("post-confirm-(id_number)@(host)", id_number="[a-z0-9]+")
def CONFIRMING(message, id_number=None, host=None):
    original = CONFIRM.verify('post', message['from'], id_number)

    if original:
        name, address = parseaddr(original['from'])
        post_name = original['x-post-name']

        post_id = post.post(post_name, address, host, original)
        msg = view.respond(locals(), "mail/welcome.msg",
                           From="noreply@%(host)s",
                           To=message['from'],
                           Subject="Welcome, your blog is ready.")
        relay.deliver(msg)

        return POSTING
    else:
        logging.warning("Invalid confirm from %s", message['from'])
        return CONFIRMING



@route("(post_name)@(host)")
@route("(post_name)-(action)@(host)", action="delete")
def POSTING(message, post_name=None, host=None, action=None):
    name, address = parseaddr(message['from'])

    if not action:
        post.post(post_name, address, host, message)
        msg = view.respond(locals(), 'mail/page_ready.msg', 
                           From="noreply@%(host)s",
                           To=message['from'],
                           Subject="Your page '%(post_name)s' is ready.")
        relay.deliver(msg)

        # first real message, now we can index it
        index_q = queue.Queue("run/posts")
        index_q.push(message)
    elif action == "delete":
        post.delete(post_name, address)

        msg = view.respond(locals(), 'mail/deleted.msg', 
                           From="noreply@%(host)s",
                           To=message['from'],
                           Subject="Your page '%(post_name)s' was deleted.")

        relay.deliver(msg)
    else:
        logging.debug("Invalid action: %r", action)

    return POSTING



������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/model/�������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017335� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/model/__init__.py��������������������������������������������������0000644�0000765�0000024�00000000000�11207053442�021422� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/model/comment.py���������������������������������������������������0000644�0000765�0000024�00000000742�11212411064�021334� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import queue


def attach_headers(message, user_id, post_name, domain):
    """Headers are used later by the index.py handler to figure out where
    the message finally goes."""
    message['X-Post-Name'] = post_name
    message['X-Post-User-ID'] = user_id
    message['X-Post-Domain'] = domain


def defer_to_queue(message):
    index_q = queue.Queue("run/posts")  # use a diff queue?
    index_q.push(message)
    print "run/posts count after dever", index_q.count()
������������������������������lamson-1.0pre11/examples/osb/app/model/post.py������������������������������������������������������0000644�0000765�0000024�00000003232�11221064147�020662� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os
import logging
from lamson import view, queue
import email
from config.settings import BLOG_BASE
from markdown import markdown


def delete(post_name, user):
    file_name = blog_file_name(post_name, user)

    if os.path.exists(file_name):
        logging.debug("DELETING %s", file_name)
        os.unlink(file_name)
        remove_from_queue(post_name, user)


def post(post_name, user, host, message):
    user_dir = make_user_dir(user)
    user_id, domain = user.split("@")

    # make sure it's removed first if it existed
    delete(post_name, user)

    posting = open("%s/%s.html" % (user_dir, post_name), "w")
    content = markdown(message.body())

    html = view.render(locals(), "web/post.html")

    posting.write(html.encode('utf-8'))

    post_q = get_user_post_queue(user_dir)
    post_q.push(message)


def make_user_dir(user):
    user_dir = get_user_dir(user)

    if not user_exists(user):
        os.mkdir(user_dir)

    return user_dir

def remove_from_queue(post_name, user):
    user_dir = get_user_dir(user)
    post_q = get_user_post_queue(user_dir)
    for k in post_q.keys():
        msg = post_q.get(k)
        name, address = email.utils.parseaddr(msg['to'])
        if address.startswith(post_name):
            logging.debug("Removing %s:%s from the queue", k, address)
            post_q.remove(k)


def user_exists(user):
    return os.path.exists(get_user_dir(user))

def get_user_dir(user):
    return "%s/%s" % (BLOG_BASE, user)

def blog_file_name(post_name, user):
    return "%s/%s.html" % (get_user_dir(user), post_name)

def get_user_post_queue(user_dir):
    queue_dir = "%s/posts_queue" % (user_dir)
    return queue.Queue(queue_dir)

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/���������������������������������������������������������0000755�0000765�0000024�00000000000�11313464573�020232� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021155� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/comment_confirm.msg���������������������������������0000644�0000765�0000024�00000001270�11221063766�025040� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi there, you seem to want to comment on this post:

{{user_id}}-AT-{{domain}}-{{post_name}}@{{host}}

Since we don't know who you are, we can't just let you go 
commenting without some kind of verification.  Someone could
trying to comment in your name or sign you up for something
you don't want.

With that in mind, simply hit reply to this message or send an email to:

{{ confirm_address }}@{{host}}

Once you send that then we'll make your page using your original
message.

** Don't worry, your original comment is not lost. **

We'll make it all happen right when you confirm your request.

Thanks!

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/comment_submitted.msg�������������������������������0000644�0000765�0000024�00000000372�11221064343�025375� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Your comment entitled:

{{original['subject']}}

Has been posted to:

{{original['to']}}

It will show up shortly, so don't go crazy if it takes a little
while.

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/confirm.msg�����������������������������������������0000644�0000765�0000024�00000001431�11221063416�023305� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi there, you requested that I create a blog with a post named:

{{ post_name }} : {{ message['subject'] }}

Before I do that, you'll just need to confirm that you are a person and
not someone pretending to be a person.  Sure, a bot could probably
figure out how to confirm this subscription, but at a minimum we want
to make sure that you aren't at least signed up for something you don't
want.

With that in mind, simply hit reply to this message or send an email to:

{{ confirm_address }}@{{host}}

Once you send that then we'll make your page using your original
message.

** Don't worry, your original posting is not lost. **

We'll make it all happen right when you confirm your request.

Thanks!

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/deleted.msg�����������������������������������������0000644�0000765�0000024�00000000341�11221064600�023251� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Your blog page at:

http://{{host}}/posts/{{address}}/{{post_name}}.html

Was DELETED, hopefully by you.  Maybe we should confirm this?

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/page_ready.msg��������������������������������������0000644�0000765�0000024�00000000377�11221064607�023763� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Your requested blog post is ready:

http://{{host}}/posts/{{address}}/{{post_name}}.html

You can delete it by sending an email to:


{{ post_name }}-delete@{{host}}

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/mail/welcome.msg�����������������������������������������0000644�0000765�0000024�00000001225�11221064442�023304� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������You are now the proud owner of:

http://{{host}}/posts/{{address}}/{{ post_name }}.html

Your first post titled "{{ original['Subject'] }}" is already posted and
you can continue to post by sending email to:

{{ post_name }}@{{ host }}

If you would like to delete the post you just made, simply send an email
to:

{{ post_name }}-delete@{{host}}

It will be removed shortly after that, but not immediately.  We'll
notify you when it is finally removed.

You can get more information on things you can do with the LamsonProject
OSB at:

http://{{host}}/help.html

Thanks!

---
OneShotBlog(TM)
http://{{host}}/
A Lamson project example http://lamsonproject.org/
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/web/�����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021010� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/web/base.html��������������������������������������������0000644�0000765�0000024�00000002733�11212630346�022603� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    {% block header %}
	<link rel="stylesheet" type="text/css" href="/styles/reset.css" />
    <link rel="stylesheet" type="text/css" href="/styles/main.css" />
    {% endblock %}

    {% block title %}
    <title>OneShotBlog(TM): Latest Posts</title>
    {% endblock %}
</head>

<body>
	<div class="wrapper">
		<div class="header">
            <h1>
                {% block display_title %}
                <a href="/index.html">One. Sh<span>o</span>t. Blog.</a>
                {% endblock %}
            </h1>
			<ul>
				<li><a href="/index.html">latest</a></li>
				<li><a href="/about.html">about</a></li>
				<li><a href="/help.html">help</a></li>
			</ul>
		</div>
		<ul class="notifications">
            {% block notifications %}
			<li>
				<div>
                    <h3>About OneShotBlog(TM)</h3>
                    <p>An example project for <a href="http://lamsonproject.org">Lamson</a>.</p>
				</div>
                </li>
            {% endblock %}
			<li class="footer">
				<div>
					<p>Software Copyright (C) 2009 Zed Shaw. Posts Copyright by their owners, sue them.</p>
				</div>
			</li>
		</ul>
        <div class="content">
            {% block content %}
            {% endblock %}
		</div>
	</div>
</body>
</html>
�������������������������������������lamson-1.0pre11/examples/osb/app/templates/web/comments.html����������������������������������������0000644�0000765�0000024�00000000250�11212632211�023477� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
<div class="post">
    <h2>{{ message['subject'] | striptags | escape | title }}</h2>
    {{ contents }}  

    <p><a href="/spam.html">[this is spam]</a></p>
</div>

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/web/index.html�������������������������������������������0000644�0000765�0000024�00000000516�11212374425�023000� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{% extends "web/base.html" %}

{% block content %}

  {% for post in posts %}
  <div class="post">
      <h2><a href="{{ post['x-post-url']}}">{{ post['subject'] | striptags | escape | title }}</a> </h2>
      <p>{{ post.body()[0:200] }}...<br/><font size="9px">({{ post['date'] }})</font></p>
  </div>
  {% endfor %}

{% endblock %}
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/app/templates/web/post.html��������������������������������������������0000644�0000765�0000024�00000001560�11212636550�022656� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{% extends "web/base.html" %}

{% block header %}
{{ super() }}
    <script type="text/javascript" src="/jquery.js"></script>
    <script type="text/javascript">
        $(document).ready(function(){
                $("#comments").empty()
                $("#comments").load("{{ post_name + "-comments.html" }}")
    });
    </script>
{% endblock %}

{% block title %}
<title>{{user}}: {{message['Subject'] | striptags | escape | title }}</title>
{% endblock %}

{% block notifications %}
{{ super() }}
<li>
    <div>
        <h3>About {{user}}</h3>
        <p>They blog here.  That's all we know.</p>
    </div>
</li>
{% endblock %}


{% block content %}
<div class="post">
    <h2>{{ message['Subject'] }}</h2>
    <a href="mailto:{{user_id}}-AT-{{domain}}-{{post_name}}-comment@{{host}}">[send a comment]</a>
    {{content}}
</div>

<div id="comments">
</div>
{% endblock %}


������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/����������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016722� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/__init__.py�����������������������������������������������������0000644�0000765�0000024�00000000000�11207053442�021007� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/boot.py���������������������������������������������������������0000644�0000765�0000024�00000001603�11227341200�020217� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])


Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

�����������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/forward.py������������������������������������������������������0000644�0000765�0000024�00000001576�11227341200�020731� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, QueueReceiver
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = QueueReceiver('run/undeliverable', settings.queue_config['sleep'])

Router.defaults(**settings.router_defaults)
Router.load(['lamson.handlers.forward'])
Router.RELOAD=True
Router.UNDELIVERABLE_QUEUE=None

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

����������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/logging.conf����������������������������������������������������0000644�0000765�0000024�00000001064�11212227315�021204� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=fileHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[logger_routing]
level=DEBUG
handlers=fileHandler
qualname=routing
propagate=0

[handler_fileHandler]
# this works using FileHandler
class=FileHandler
# If you have Python2.6 you can use this and it will work when you use logrotate
#class=WatchedFileHandler
level=DEBUG
formatter=defaultFormatter
args=("logs/lamson.log",)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/queue.py��������������������������������������������������������0000644�0000765�0000024�00000001610�11227341201�020377� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, QueueReceiver
from lamson import view
import logging
import logging.config
import jinja2

# configure logging to go to a log file
logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = QueueReceiver(settings.queue_config['queue'],
                                  settings.queue_config['sleep'])


Router.defaults(**settings.router_defaults)
Router.load(settings.queue_handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/settings.py�����������������������������������������������������0000644�0000765�0000024�00000002054�11251317352�021125� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file contains python variables that configure Lamson for email processing.
from lamson import queue, routing, confirm
import logging
import shelve

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

handlers = ['app.handlers.post', 'app.handlers.comment']

router_defaults = {'host': 'oneshotblog\\.com', 
                   'domain': "localhost|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}",
                   'user_id': "[a-zA-Z0-9._%+-]+",
                   'post_name': "[a-zA-Z0-9][a-zA-Z0-9.]+"}

template_config = {'dir': 'app', 'module': 'templates'}

BLOG_BASE="app/data/posts"

# this is for when you run the config.queue boot
queue_config = {'queue': 'run/posts', 'sleep': 10}

queue_handlers = ['app.handlers.index']

SPAM = {'db': 'app/spamdb', 'rc': 'spamrc', 'queue': 'run/spam'}

routing.Router.UNDELIVERABLE_QUEUE=queue.Queue("run/undeliverable")

CONFIRM_STORAGE=confirm.ConfirmationStorage(db=shelve.open("run/confirmationsdb"))
CONFIRM = confirm.ConfirmationEngine('run/pending', CONFIRM_STORAGE)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/test_logging.conf�����������������������������������������������0000644�0000765�0000024�00000001042�11212622470�022240� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=stdoutHandler,stderrHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=stdoutHandler

[logger_routing]
level=DEBUG
handlers=stderrHandler
qualname=routing
propagate=0

[handler_stdoutHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)

[handler_stderrHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stderr,)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/config/testing.py������������������������������������������������������0000644�0000765�0000024�00000002146�11227341201�020735� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson import view
from lamson.routing import Router
from lamson.server import Relay
import jinja2
import logging
import logging.config
import os

# configure logging to go to a log file
logging.config.fileConfig("config/test_logging.conf")

# the relay host to actually send the final message to (set debug=1 to see what
# the relay is saying to the log server).
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=0)


settings.receiver = None


Router.defaults(**settings.router_defaults)
Router.load(settings.handlers + settings.queue_handlers)
Router.RELOAD=True

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

# if you have pyenchant and enchant installed then the template tests will do
# spell checking for you, but you need to tell pyenchant where to find itself
if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
    os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/doc/�������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016222� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/doc/done.txt�����������������������������������������������������������0000644�0000765�0000024�00000000000�11212336424�017664� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/doc/report.txt���������������������������������������������������������0000644�0000765�0000024�00000000000�11212336424�020252� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/doc/todo.txt�����������������������������������������������������������0000644�0000765�0000024�00000000362�11212412251�017710� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Use jinja template inheritance.
Create an anonymize conversion for the published emails, route them through lamson.
Create an address they can forward spam to.
Add a this is spam link to the pages and use web server logs to train and remove.
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/muttrc�����������������������������������������������������������������0000644�0000765�0000024�00000000422�11211246357�016706� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
set from="Test Tester <tester@somehost.com>"
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/pendingrc��������������������������������������������������������������0000644�0000765�0000024�00000000351�11211573141�017334� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������set mbox_type=Maildir
set folder="run/posts"
set mask="!^\\.[^.]"
set mbox="run/posts"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/posts"
set sendmail="/usr/local/bin/lamson sendmail -port 8823 -host 127.0.0.1"


���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/README�����������������������������������������������������������������0000644�0000765�0000024�00000000624�11207361340�016324� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������OSB is a One-Shot-Blog application that makes blog pages for people
using email messages.  It is meant as a demonsration of the newer
routing and Lamson design until I can update the mailing list
application.

To try it do this:

lamson log
lamson start
nosetests
mutt -F muttrc
mutt -F pendingrc

Then you'll see all the gear.  You can use mutt -F muttrc to actually
interact with the running server.


������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/�����������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016617� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/handlers/��������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020417� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/handlers/__init__.py���������������������������������������������0000644�0000765�0000024�00000000000�11207053442�022504� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/handlers/comments_tests.py���������������������������������������0000644�0000765�0000024�00000003646�11251316554�024044� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson.routing import Router
from app.model import post
import time

sender = "sender-%s@localhost" % time.time()
host = "oneshotblog.com"
comment_id = int(time.time())
comment_address = "tester-AT-localhost-test.blog.%d-comment@%s" % (comment_id, host)
target_user = "tester@localhost"
sender = "commenter-%s@localhost" % time.time()
client = RouterConversation(sender, 'Comment Tests Subject')

def setup():
    clear_queue("run/posts")
    clear_queue("run/spam")
    post.make_user_dir(target_user)

def make_spam():
    spam_data = open("tests/spam").read()
    spam = mail.MailRequest("test_spam_sent_by_unconfirmed_user", "spammer@spamtime.com", "spam" + comment_address, spam_data)
    spam['To'] = "spam" + comment_address
    return spam

def test_new_user_comments():
    client.begin()
    msg = client.say(comment_address, "I totally disagree with you!", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')
    assert delivered(sender, to_queue=queue("run/posts"))
    assert delivered(sender, to_queue=queue(post.get_user_dir(target_user) + "/comments"))


def test_confirmed_user_comments():
    test_new_user_comments()
    client.say(comment_address, "I said I disagree!", "noreply")
    assert delivered(sender, to_queue=queue("run/posts"))

def test_invalid_confirmation():
    client.begin()
    client.say(comment_address, "I want to break in.", 'confirm')
    clear_queue()  # make sure no message is available

    # attacker does not have the above message
    client.say("confirm-11111111@" + host, 'Sneaky I am.')
    assert not delivered('noreply'), "Should not get a reply to a bad confirm." + str(msg)

def test_spam_sent_by_unconfirmed_user():
    setup()

    client.begin()
    Router.deliver(make_spam())

def test_spam_sent_by_confirmed_user():
    test_confirmed_user_comments()
    clear_queue("run/posts")

    Router.deliver(make_spam())


������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/handlers/index_tests.py������������������������������������������0000644�0000765�0000024�00000001350�11212624242�023305� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from handlers import post_tests
import os

index = "app/data/index.html"

def reset_index():
    clear_queue("run/indexed")
    assert queue("run/indexed").count() == 0

    clear_queue("run/posts")
    assert queue("run/posts").count() == 0
    if os.path.exists(index):
        os.unlink(index)

def setup():
    reset_index()

def test_index_updated_after_post():
    post_tests.test_new_user_subscribes()
    assert os.path.exists(index)
    contents = open(index).read()

    post_tests.test_existing_user_posts()
    assert os.path.exists(index)
    updated = open(index).read()

    assert contents != updated, "The index should change."

def test_comment_added_to_list():
    pass

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/handlers/post_tests.py�������������������������������������������0000644�0000765�0000024�00000003401�11212624141�023160� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
import os
import time
import shutil

relay = relay(port=8823)
sender = "sender-%s@sender.com" % time.time()
host = "oneshotblog.com"
blog_id = int(time.time())
blog_address = "test.blog.%d@%s" % (blog_id, host)

client = RouterConversation(sender, 'Post Tests Subject')

def test_new_user_subscribes():
    client.begin()
    msg = client.say(blog_address, "I'd like a blog thanks.", 'confirm')
    client.say(msg['Reply-To'], 'Confirmed I am.', 'noreply')

def test_bad_user_tries_invalid_confirm():
    client.begin()
    client.say(blog_address, "I want to break in.", 'confirm')
    clear_queue()  # make sure no message is available

    # attacker does not have the above message
    client.say("confirm-11111111@" + host, 'Sneaky I am.')
    assert not delivered('noreply'), "Should not get a reply to a bad confirm." + str(msg)


def test_existing_user_posts():
    test_new_user_subscribes()

    client.say(blog_address, "This is my new page.", "noreply")

    expected_file = "app/data/posts/%s/%s.html" % (sender, 'test.blog.%s' % blog_id)
    assert os.path.exists(expected_file), "Should get an html."


def test_existing_user_posts_invalid_action():
    test_new_user_subscribes()
    clear_queue()

    client.say("test.blog.%s-unfuddleamick@" + host, 'Please unfuddleamick me.')
    assert not delivered('noreply'), "Should get nothing for an invalid action."

def test_existing_user_deletes():
    test_new_user_subscribes()
    clear_queue()

    expected_file = "app/data/posts/%s/%s.html" % (sender, 'test.blog.%s' % blog_id)

    blog_delete = "test.blog.%s-delete@%s" % (blog_id, host)
    client.say(blog_delete, "Please delete.", "noreply")

    assert not os.path.exists(expected_file), "File should be gone."



���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/model/�����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017717� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/model/__init__.py������������������������������������������������0000644�0000765�0000024�00000000000�11207053442�022004� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/model/comment.py�������������������������������������������������0000644�0000765�0000024�00000000566�11251313056�021727� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson import mail
from app.model import comment

def test_attach_headers():
    msg = mail.MailRequest('test_attach_headers', 'tester@localhost', 'test.blog@oneshotblog.com',
                           'Fake body.')

    comment.attach_headers(msg)
    for key in ['X-Post-Name', 'X-Post-User-ID', 'X-Post-Domain']:
        assert key in msg

������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/model/post_tests.py����������������������������������������������0000644�0000765�0000024�00000003640�11251313067�022472� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.mail import MailRequest
from lamson import view
import os
import time
from app.model import post
import jinja2
import config
import shutil

view.LOADER = jinja2.Environment(loader=jinja2.PackageLoader('app', 'templates'))
user = "test_user@localhost"
blog = "test_blog"
name = "Tester Joe"

def test_post():
    message = MailRequest("fakepeer", user,
                          "%s@oneshotblog.com" % blog, "Fake body")
    message['Subject'] = 'Test subject'

    post.post(blog, user, "localhost", message)

    assert post.user_exists(user), "User dir not created."
    assert os.path.exists(post.blog_file_name(blog, user)), "File not made."

def test_delete():
    test_post()
    post.delete(blog, user)
    assert post.user_exists(user), "User dir should stay."
    assert not os.path.exists(post.blog_file_name(blog, user)), "File should go."

def test_make_user_dir():
    assert not os.path.exists("sampleuser")
    dir = post.make_user_dir("sampleuser")
    assert dir == post.get_user_dir("sampleuser")
    assert os.path.exists(dir)
    shutil.rmtree(dir)


def test_remove_from_queue():
    message = MailRequest("fakepeer", user,
                          "%s@oneshotblog.com" % blog, "Fake body")
    message['Subject'] = 'Test subject'

    post_q = post.get_user_post_queue(post.get_user_dir(user))

    post.post(blog, user, 'localhost', message)

    assert post_q.count(), "No messages in the post queue."
    count = post_q.count()

    post.remove_from_queue(blog, user)
    assert post_q.count() == count-1, "It didn't get removed."

def test_user_exists():
    assert post.user_exists(user)
    assert not post.user_exists(user + "nothere")

def test_get_user_dir():
    dir = post.get_user_dir(user)
    assert dir.startswith(config.settings.BLOG_BASE)
    assert dir.endswith(user)

def test_blog_file_name():
    name = post.blog_file_name(blog, user)
    assert name.endswith("html")


������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/spam�������������������������������������������������������������0000600�0000765�0000024�00000003236�11224036657�017474� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������From fibrous_favorate@yahoo.co.jp  Mon May 18 14:39:07 2009
Return-Path: <fibrous_favorate@yahoo.co.jp>
X-Original-To: zedshaw@zedshaw.com
Delivered-To: zedshaw@zedshaw.com
Received: from localhost.localdomain (localhost [127.0.0.1])
	by mail.zedshaw.com (Postfix) with ESMTP id E67BC1ECD49
	for <zedshaw@zedshaw.com>; Mon, 18 May 2009 14:39:07 -0700 (PDT)
From: =?iso-2022-jp?B?Zmlicm91c19mYXZvcmF0ZUB5YWhvby5jby5qcA==?=<fibrous_favorate@yahoo.co.jp>
Subject: =?iso-2022-jp?B?GyRCJS8lQSUzJV83TyU1JSQlSBsoQjA1?=
MIME-Version: 1.0
Reply-To: <fibrous_favorate@yahoo.co.jp>
Date: Mon, 18 May 2009 21:26:30 +0900
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit
X-Spambayes-Classification: ham; 0.00
Message-Id: <20090518213907.E67BC1ECD49@mail.zedshaw.com>
Status: O
Content-Length: 842
Lines: 37
X-Spambayes-Trained: spam


$B$I$&$b(B

$B:#2s$b%"%s%0%i7O!"%/%A%3%_7O$N%5%$%H$r=d2s$7$F$_$^$7$?!#(B

$B$3$3:G6a?M:J7O$KFC2=$7$?%5%$%H$r$4>R2p$7$F$*$j$^$7$?$,(B

$B!J<+J,$N<qL#A43+!*!*!K(B

$B:#2s$O%3%_%e%K%F%#!<?'$N6/$$%5%$%H$r$4>R2p!#(B

$B$^$"!"%a%8%c!<$H0c$C$F%H%C%W%Z!<%8$OL#5$$J$$$G$9$,!"(B

$B%/%A%3%_7O$G$O8+47$l$?Iw7J#w$G$9!#(B

$B$H$j$"$($:!"EPO?$b%A%c%A%c$C$H4JC1$G$9!#(B

$B5!G=E*$K$O3d$j$H$A$c$s$H$7$F$F!J<:Ni$+#w!K(B

$B<L%a!"(BPC$B!&%b%P%$%kN>J}(BOK$B!"$*CN$i$;5!G=!"(Betc

$B%3%_%e%K%1!<%7%g%s$KI,MW$JItJ,$O$*$5$($F$"$j$^$9!#(B

$BG/NpAX$O<c$/$bL5$/G/4s$j$G$bL5$/!"(BOL$B7O$,Cf?4$+$H;W$$$^$9!#(B

$B$^$"!"2q<R5"$j$K0{$_$K9T$C$?$j!"HS$r?)$$$K9T$C$?$j$NM6$$$O(B

$B3d$H$9$s$J$j9T$/$h$&$G$9#w(B

$B$=$N8e$O!"$^$"!"KM$+$i$O2?$H$b#w#w(B

$BJ70O5$$G$&$^$/$d$C$F$/$@$5$$#w(B

[$B%3%_%e%3%_%e(B]
http://commu-commu.net


������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/templates/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020615� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/templates/__init__.py��������������������������������������������0000644�0000765�0000024�00000000000�11207053442�022702� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/examples/osb/tests/templates/osb_tests.py�������������������������������������������0000644�0000765�0000024�00000000542�11221064147�023163� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
from lamson import view
import os
from glob import glob

def test_spelling():
    message = {}
    original = {}
    for path in glob("app/templates/mail/*.msg"):
        template = "mail/" + os.path.basename(path)
        result = view.render(locals(), template)
        spelling(template, result)

��������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/�����������������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�014345� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/__init__.py������������������������������������������������������������������0000644�0000765�0000024�00000000000�11165520647�016443� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/args.py����������������������������������������������������������������������0000644�0000765�0000024�00000025112�11241322716�015643� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Implements Lamson's command line argument parsing system.  It is honestly
infinitely better than optparse or argparse so it will be released later as a
separate library under the BSD license.

It's used very easily.  First, you write a module that is like lamson.commands.
Each function name BLAH_command implements a sub-command.  Then you use
lamson.args.parse_and_run_command to parse the command line and run the function
that matches.

Note that the _command suffix is optional and configurable, but it is there
to disambiguate your commands so you can use Python reserved words and base
types as your command names.  Without it, you can do a list_command or a
for_command.

You command then specifies its keyword arguments to indicate what has
reasonable defaults and what is required.  Give a value to the option
to indicate its default, and give a None setting to indicate it is required.
A good way to read this is it is your commands "default settings" and None
says "this option has no default setting".

Here's an example from lamson:


   def send_command(port=8825, host='127.0.0.1', debug=1, sender=None, to=None,
                 subject=None, body=None, file=False):

You can see this has subject, body, sender, and to as required options (they 
are None), and the rest have some default value.

With this the argument parser will parse the users given arguments, and then
call your command function with those as keyword arguments, but after it has
fixed them up with the defaults you gave.  In the event that a user does
not give a required option, lamson.args will abort with an error telling them.

Lamson's argument parser also accurately detects and parses integers, boolean
values, strings, emails, single word values, and can handle trailing arguments
after a -- argument.  This means you don't have to do conversion, it should be
the right type for what you expect.

Lamson.args does not care if you use one dash (-help), two dashes
(--help), three dashes (---help) or a billion.  In all honesty, who gives a
rat's ass, just get the user to type something like a dash followed by a word and
that's good enough.

If you just need argument parsing and no commands then you can just use
lamson.args.parse directly.

Finally, the help documentation for your commands is just the __doc__
string of the function.
"""

import re
import sys
import inspect


S_IP_ADDRESS = lambda x, token: ['ip_address', token]
S_WORD = lambda x, token:  ['word', token]
S_EMAIL_ADDR = lambda x, token:  ['email', token]
S_OPTION = lambda x, token:  ['option', token.split("-")[-1]]
S_INT = lambda x, token:  ['int', int(token) ]
S_BOOL = lambda x, token:  ['bool', bool(token) ]
S_EMPTY = lambda x, token:  ['empty', '']
S_STRING = lambda x, token:  ['string', token]
S_TRAILING = lambda x, token:  ['trailing', None]

class ArgumentError(Exception): 
    """Thrown when lamson.args encounters a command line format error."""
    pass


SCANNER = re.Scanner([
    (r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", S_EMAIL_ADDR),
    (r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]", S_IP_ADDRESS),
    (r"-+[a-zA-Z0-9]+", S_OPTION),
    (r"True", S_BOOL),
    (r"[0-9]+", S_INT),
    (r"--", S_TRAILING),
    (r"[a-z\-]+", S_WORD),
    (r"\s", S_EMPTY),
    (r".+", S_STRING),
])


def match(tokens, of_type = None):
    """
    Responsible for taking a token off and processing it, ensuring it is 
    of the correct type.  If of_type is None (the default) then you are
    asking for anything.
    """
    # check the type (first element)
    if of_type:
        if not peek(tokens, of_type):
            raise ArgumentError("Expecting '%s' type of argument not %s in tokens: %r.  Read the lamson help." % 
                               (of_type, tokens[0][0], tokens))

    # take the token off the front
    tok = tokens.pop(0)

    # return the value (second element)
    return tok[1]


def peek(tokens, of_type):
    """Returns true if the next token is of the type, false if not.  It does not
    modify the token stream the way match does."""
    if len(tokens) == 0:
        raise ArgumentError("This command expected more on the command line.  Not sure how you did that.")

    return tokens[0][0] == of_type


def trailing_production(data, tokens):
    """Parsing production that handles trailing arguments after a -- is given."""
    data['TRAILING'] = [x[1] for x in tokens]
    del tokens[:]

def option_production(data, tokens):
    """The Option production, used for -- or - options.  The number of - aren't 
    important.  It will handle either individual options, or paired options."""
    if peek(tokens, 'trailing'):
        # this means the rest are trailing arguments, collect them up
        match(tokens, 'trailing')
        trailing_production(data, tokens)
    else:
        opt = match(tokens, 'option')
        if not tokens:
            # last one, it's just true
            data[opt] = True
        elif peek(tokens, 'option') or peek(tokens, 'trailing'):
            # the next one is an option so just set this to true
            data[opt] = True
        else:
            # this option is set to something else, so we'll grab that
            data[opt] = match(tokens)


def options_production(tokens):
    """List of options, optionally after the command has already been taken off."""
    data = {}
    while tokens:
        option_production(data, tokens)
    return data


def command_production(tokens):
    """The command production, just pulls off a word really."""
    return match(tokens, 'word')


def tokenize(argv):
    """Goes through the command line args and tokenizes each one, trying to match
    something in the scanner.  If any argument doesn't completely parse then it
    is considered a 'string' and returned raw."""

    tokens = []
    for arg in argv:
        toks, remainder = SCANNER.scan(arg)
        if remainder or len(toks) > 1:
            tokens.append(['string', arg])
        else:
            tokens += toks
    return tokens


def parse(argv):
    """
    Tokenizes and then parses the command line as wither a command style or
    plain options style argument list.  It determines this by simply if the
    first argument is a 'word' then it's a command.  If not then it still
    returns the first element of the tuple as None.  This means you can do:

        command, options = args.parse(sys.argv[1:])

    and if command==None then it was an option style, if not then it's a command 
    to deal with.
    """
    tokens = tokenize(argv)
    if not tokens:
        return None, {}
    elif peek(tokens, "word"):
        # this is a command style argument
        return command_production(tokens), options_production(tokens)
    else:
        # options only style
        return None, options_production(tokens)


def determine_kwargs(function):
    """
    Uses the inspect module to figure out what the keyword arguments
    are and what they're defaults should be, then creates a dict with
    that setup.  The results of determine_kwargs() is typically handed
    to ensure_defaults().
    """
    spec = inspect.getargspec(function)
    keys = spec[0]
    values = spec[-1]
    result = {}
    for i in range(0, len(keys)):
        result[keys[i]] = values[i]
    return result

def ensure_defaults(options, reqs):
    """
    Goes through the given options and the required ones and does the
    work of making sure they match.  It will raise an ArgumentError
    if any option is required.  It will also detect that required TRAILING
    arguments were not given and raise a separate error for that.
    """
    for key in reqs:
        if reqs[key] == None:
            # explicitly set to required
            if key not in options:
                if key == "TRAILING":
                    raise ArgumentError("Additional arguments required after a -- on the command line.")
                else:
                    raise ArgumentError("Option -%s is required by this command." % key)
        else:
            if key not in options:
                options[key] = reqs[key]

def command_module(mod, command, options, ending="_command"):
    """Takes a module, uses the command to run that function."""
    function = mod.__dict__[command+ending]
    kwargs = determine_kwargs(function)
    ensure_defaults(options, kwargs)
    try:
        return function(**options)
    except TypeError, exc:
        print "ERROR: ", exc


def available_help(mod, ending="_command"):
    """Returns the dochelp from all functions in this module that have _command
    at the end."""
    help_text = []
    for key in mod.__dict__:
        if key.endswith(ending):
            name = key.split(ending)[0]
            help_text.append(name + ":\n" + mod.__dict__[key].__doc__)

    return help_text


def help_for_command(mod, command, ending="_command"):
    """
    Returns the help string for just this one command in the module.
    If that command doesn't exist then it will return None so you can
    print an error message.
    """

    if command in available_commands(mod):
        return mod.__dict__[command + ending].__doc__
    else:
        return None


def available_commands(mod, ending="_command"):
    """Just returns the available commands, rather than the whole long list."""
    commands = []
    for key in mod.__dict__:
        if key.endswith(ending):
            commands.append(key.split(ending)[0])

    commands.sort()
    return commands


def invalid_command_message(mod, exit_on_error):
    """Called when you give an invalid command to print what you can use."""
    print "You must specify a valid command.  Try these: "
    print ", ".join(available_commands(mod))

    if exit_on_error: 
        sys.exit(1)
    else:
        return False


def parse_and_run_command(argv, mod, default_command=None, exit_on_error=True):
    """
    A one-shot function that parses the args, and then runs the command
    that the user specifies.  If you set a default_command, and they don't
    give one then it runs that command.  If you don't specify a command,
    and they fail to give one then it prints an error.

    On this error (failure to give a command) it will call sys.exit(1).
    Set exit_on_error=False if you don't want this behavior, like if
    you're doing a unit test.
    """
    try:
        command, options = parse(argv)

        if not command and default_command:
            command = default_command
        elif not command and not default_command:
            return invalid_command_message(mod, exit_on_error)

        if command not in available_commands(mod):
            return invalid_command_message(mod, exit_on_error)

        command_module(mod, command, options)
    except ArgumentError, exc:
        print "ERROR: ", exc
        if exit_on_error:
            sys.exit(1)

    return True

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/bounce.py��������������������������������������������������������������������0000644�0000765�0000024�00000027262�11241324126�016167� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Bounce analysis module for Lamson.  It uses an algorithm that tries
to simply collect the headers that are most likely found in a bounce
message, and then determine a probability based on what it finds.
"""

import re
from functools import wraps


BOUNCE_MATCHERS = {
    'Action': re.compile(r'(failed|delayed|delivered|relayed|expanded)', re.IGNORECASE | re.DOTALL),
    'Content-Description': re.compile(r'(Notification|Undelivered Message|Delivery Report)', re.IGNORECASE | re.DOTALL),
    'Diagnostic-Code': re.compile(r'(.+);\s*([0-9\-\.]+)?\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Final-Recipient': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Received': re.compile(r'(.+)', re.IGNORECASE | re.DOTALL),
    'Remote-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Reporting-Mta': re.compile(r'(.+);\s*(.*)', re.IGNORECASE | re.DOTALL),
    'Status': re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)', re.IGNORECASE | re.DOTALL)
}

BOUNCE_MAX = len(BOUNCE_MATCHERS) * 2.0

PRIMARY_STATUS_CODES = {
    u'1': u'Unknown Status Code 1',
    u'2': u'Success',
    u'3': u'Temporary Failure',
    u'4': u'Persistent Transient Failure',
    u'5': u'Permanent Failure'
}

SECONDARY_STATUS_CODES = {
    u'0':   u'Other or Undefined Status',
    u'1':   u'Addressing Status',
    u'2':   u'Mailbox Status',
    u'3':   u'Mail System Status',
    u'4':   u'Network and Routing Status',
    u'5':   u'Mail Delivery Protocol Status',
    u'6':   u'Message Content or Media Status',
    u'7':   u'Security or Policy Status',
}

COMBINED_STATUS_CODES = {
    u'00': u'Not Applicable',
    u'10': u'Other address status',
    u'11': u'Bad destination mailbox address',
    u'12': u'Bad destination system address',
    u'13': u'Bad destination mailbox address syntax',
    u'14': u'Destination mailbox address ambiguous',
    u'15': u'Destination mailbox address valid',
    u'16': u'Mailbox has moved',
    u'17': u'Bad sender\'s mailbox address syntax',
    u'18': u'Bad sender\'s system address',

    u'20': u'Other or undefined mailbox status',
    u'21': u'Mailbox disabled, not accepting messages',
    u'22': u'Mailbox full',
    u'23': u'Message length exceeds administrative limit.',
    u'24': u'Mailing list expansion problem',

    u'30': u'Other or undefined mail system status',
    u'31': u'Mail system full',
    u'32': u'System not accepting network messages',
    u'33': u'System not capable of selected features',
    u'34': u'Message too big for system',

    u'40': u'Other or undefined network or routing status',
    u'41': u'No answer from host',
    u'42': u'Bad connection',
    u'43': u'Routing server failure',
    u'44': u'Unable to route',
    u'45': u'Network congestion',
    u'46': u'Routing loop detected',
    u'47': u'Delivery time expired',

    u'50': u'Other or undefined protocol status',
    u'51': u'Invalid command',
    u'52': u'Syntax error',
    u'53': u'Too many recipients',
    u'54': u'Invalid command arguments',
    u'55': u'Wrong protocol version',

    u'60': u'Other or undefined media error',
    u'61': u'Media not supported',
    u'62': u'Conversion required and prohibited',
    u'63': u'Conversion required but not supported',
    u'64': u'Conversion with loss performed',
    u'65': u'Conversion failed',

    u'70': u'Other or undefined security status',
    u'71': u'Delivery not authorized, message refused',
    u'72': u'Mailing list expansion prohibited',
    u'73': u'Security conversion required but not possible',
    u'74': u'Security features not supported',
    u'75': u'Cryptographic failure',
    u'76': u'Cryptographic algorithm not supported',
    u'77': u'Message integrity failure',
}

def match_bounce_headers(msg):
    """
    Goes through the headers in a potential bounce message recursively
    and collects all the answers for the usual bounce headers.
    """
    matches = {'Content-Description-Parts': {}}
    for part in msg.base.walk():
        for k in BOUNCE_MATCHERS:
            if k in part.headers:
                if k not in matches:
                    matches[k] = set()

                # kind of an odd place to put this, but it's the easiest way
                if k == 'Content-Description':
                    matches['Content-Description-Parts'][part.headers[k].lower()] = part

                matches[k].add(part.headers[k])

    return matches


def detect(msg):
    """
    Given a message, this will calculate a probability score based on
    possible bounce headers it finds and return a lamson.bounce.BounceAnalyzer
    object for further analysis.

    The detection algorithm is very simple but still accurate.  For each header
    it finds it adds a point to the score.  It then uses the regex in BOUNCE_MATCHERS
    to see if the value of that header is parseable, and if it is it adds another
    point to the score.  The final probability is based on how many headers and matchers
    were found out of the total possible.

    Finally, a header will be included in the score if it doesn't match in value, but
    it WILL NOT be included in the headers used by BounceAnalyzer to give you meanings
    like remote_mta and such.

    Because this algorithm is very dumb, you are free to add to BOUNCE_MATCHERS in your
    boot files if there's special headers you need to detect in your own code.
    """
    originals = match_bounce_headers(msg)
    results = {'Content-Description-Parts':
               originals['Content-Description-Parts']}
    score = 0
    del originals['Content-Description-Parts']

    for key in originals:
        score += 1  # score still goes up, even if value doesn't parse
        r = BOUNCE_MATCHERS[key]

        scan = (r.match(v) for v in originals[key])
        matched = [m.groups() for m in scan if m]

        # a key is counted in the score, but only added if it matches
        if len(matched) > 0:
            score += len(matched) / len(originals[key])
            results[key] = matched

    return BounceAnalyzer(results, score / BOUNCE_MAX)


class BounceAnalyzer(object):
    """
    BounceAnalyzer collects up the score and the headers and gives more
    meaningful interaction with them.  You can keep it simple and just use
    is_hard, is_soft, and probable methods to see if there was a bounce.
    If you need more information then attributes are set for each of the following:

        * primary_status -- The main status number that determines hard vs soft.
        * secondary_status -- Advice status.
        * combined_status -- the 2nd and 3rd number combined gives more detail.
        * remote_mta -- The MTA that you sent mail to and aborted.
        * reporting_mta -- The MTA that was sending the mail and has to report to you.
        * diagnostic_codes -- Human readable codes usually with info from the provider.
        * action -- Usually 'failed', and turns out to be not too useful.
        * content_parts -- All the attachments found as a hash keyed by the type.
        * original -- The original message, if it's found.
        * report -- All report elements, as lamson.encoding.MailBase raw messages.
        * notification -- Usually the detailed reason you bounced.
    """
    def __init__(self, headers, score):
        """
        Initializes all the various attributes you can use to analyze the bounce
        results.
        """
        self.headers = headers
        self.score = score

        if 'Status' in self.headers:
            status = self.headers['Status'][0]
            self.primary_status = int(status[0]), PRIMARY_STATUS_CODES[status[0]]
            self.secondary_status = int(status[1]), SECONDARY_STATUS_CODES[status[1]]
            combined = "".join(status[1:])
            self.combined_status = int(combined), COMBINED_STATUS_CODES[combined]
        else:
            self.primary_status = (None, None)
            self.secondary_status = (None, None)
            self.combined_status = (None, None)

        if 'Remote-Mta' in self.headers:
            self.remote_mta = self.headers['Remote-Mta'][0][1]
        else:
            self.remote_mta = None

        if 'Reporting-Mta' in self.headers:
            self.reporting_mta = self.headers['Reporting-Mta'][0][1]
        else:
            self.reporting_mta = None

        if 'Final-Recipient' in self.headers:
            self.final_recipient = self.headers['Final-Recipient'][0][1]
        else:
            self.final_recipient = None

        if 'Diagnostic-Code' in self.headers:
            self.diagnostic_codes = self.headers['Diagnostic-Code'][0][1:]
        else:
            self.diagnostic_codes = [None, None]
       
        if 'Action' in self.headers:
            self.action = self.headers['Action'][0][0]
        else:
            self.action = None

        # these are forced lowercase because they're so damn random
        self.content_parts = self.headers['Content-Description-Parts']
        # and of course, this isn't the original original, it's the wrapper
        self.original = self.content_parts.get('undelivered message', None)

        if self.original and self.original.parts:
            self.original = self.original.parts[0]

        self.report = self.content_parts.get('delivery report', None)
        if self.report and self.report.parts:
            self.report = self.report.parts

        self.notification = self.content_parts.get('notification', None)


    def is_hard(self):
        """
        Tells you if this was a hard bounce, which is determined by the message
        being a probably bounce with a primary_status greater than 4.
        """
        return self.probable() and self.primary_status[0] > 4

    def is_soft(self):
        """Basically the inverse of is_hard()"""
        return self.probable() and self.primary_status[0] <= 4

    def probable(self, threshold=0.3):
        """
        Determines if this is probably a bounce based on the score 
        probability.  Default threshold is 0.3 which is conservative.
        """
        return self.score > threshold

    def error_for_humans(self):
        """
        Constructs an error from the status codes that you can print to
        a user.
        """
        if self.primary_status[0]:
            return "%s, %s, %s" % (self.primary_status[1],
                                   self.secondary_status[1],
                                   self.combined_status[1])
        else:
            return "No status codes found in bounce message."


class bounce_to(object):
    """
    Used to route bounce messages to a handler for either soft or hard bounces.
    Set the soft/hard parameters to the function that represents the handler.
    The function should take one argument of the message that it needs to handle
    and should have a route that handles everything.

    WARNING: You should only place this on the START of modules that will
    receive bounces, and every bounce handler should return START.  The reason
    is that the bounce emails come from *mail daemons* not the actual person
    who bounced.  You can find out who that person is using
    message.bounce.final_recipient.  But the bounce handler is *actually*
    interacting with a message from something like MAILER-DAEMON@somehost.com.
    If you don't go back to start immediately then you will mess with the state
    for this address, which can be bad.
    """
    def __init__(self, soft=None, hard=None):
        self.soft = soft
        self.hard = hard

        assert self.soft and self.hard, "You must give at least soft and/or hard"

    def __call__(self, func):
        @wraps(func)
        def bounce_wrapper(message, *args, **kw):
            if message.is_bounce():
                if message.bounce.is_soft():
                    return self.soft(message)
                else:
                    return self.hard(message)
            else:
                return func(message, *args, **kw)

        return bounce_wrapper

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/commands.py������������������������������������������������������������������0000644�0000765�0000024�00000031666�11313460103�016514� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Implements the Lamson command line tool's commands, which are run
by the lamson.args module dynamically.  Each command has it's
actual user displayed command line documentation as the __doc__
string.

You will notice that all of the command functions in this module
end in _command.  This is not required by the lamson.args module
but it is the default.  You could easily use any other suffix, or
none at all.

This is done to disambiguate the command that it implements
so that your command line tools do not clash with Python's
reserved words and built-ins.  With this design you can have a
list_command without clashing with list().

You will also notice that commands which take trailing positional
arguments give a TRAILING=[] or TRAILING=None (if it's required).
This is done instead of *args because we need to use None to indicate
that this command requires positional arguments.  TRAILING=[] is 
like saying they are optional (but expected), and TRAILING=None is
like saying they are required.  You can't (afaik) do TRAILING=None
with *args.

See lamson.args for more details.
"""

from lamson import server, args, utils, mail, routing, queue, encoding
from pkg_resources import resource_stream
from zipfile import ZipFile
import glob
import lamson
import os
import signal
import sys
import time
import mailbox
import email

def log_command(port=8825, host='127.0.0.1', chroot=False,
                chdir=".", uid=False, gid=False, umask=False, pid="./run/log.pid",
               FORCE=False):
    """
    Runs a logging only server on the given hosts and port.  It logs
    each message it receives and also stores it to the run/queue 
    so that you can make sure it was received in testing.

    lamson log -port 8825 -host 127.0.0.1 \\
            -pid ./run/log.pid -chroot False  \\
            -chdir "." -umask False -uid False -gid False \\
            -FORCE False

    If you specify a uid/gid then this means you want to first change to
    root, set everything up, and then drop to that UID/GID combination.
    This is typically so you can bind to port 25 and then become "safe"
    to continue operating as a non-root user.

    If you give one or the other, this it will just change to that
    uid or gid without doing the priv drop operation.
    """
    loader = lambda: utils.make_fake_settings(host, port)
    utils.start_server(pid, FORCE, chroot, chdir, uid, gid, umask, loader)


def send_command(port=8825, host='127.0.0.1', username=False, password=False,
                 ssl=False, starttls=False, debug=1, sender=None, to=None,
                 subject=None, body=None, attach=False):
    """
    Sends an email to someone as a test message.
    See the sendmail command for a sendmail replacement.
    
    lamson send -port 8825 -host 127.0.0.1 -debug 1 \\
            -sender EMAIL -to EMAIL -subject STR -body STR -attach False'

    There is also a username, password, and starttls option for those 
    who need it.
    """
    message = mail.MailResponse(From=sender,
                                  To=to,
                                  Subject=subject,
                                  Body=body)
    if attach:
        message.attach(attach)

    if username == False:
        username = None
    if password == False:
        password = None

    relay = server.Relay(host, port=port, username=username, password=password,
                         ssl=ssl, starttls=starttls, debug=debug)
    relay.deliver(message)


def sendmail_command(port=8825, host='127.0.0.1', debug=0, TRAILING=None):
    """
    Used as a testing sendmail replacement for use in programs
    like mutt as an MTA.  It reads the email to send on the stdin
    and then delivers it based on the port and host settings.

    lamson sendmail -port 8825 -host 127.0.0.1 -debug 0 -- [recipients]
    """
    relay = server.Relay(host, port=port,
                           debug=debug)
    data = sys.stdin.read()
    msg = mail.MailRequest(None, TRAILING, None, data)
    relay.deliver(msg)




def start_command(pid='./run/smtp.pid', FORCE=False, chroot=False, chdir=".",
                  boot="config.boot", uid=False, gid=False, umask=False):
    """
    Runs a lamson server out of the current directory:

    lamson start -pid ./run/smtp.pid -FORCE False -chroot False -chdir "." \\
            -umask False -uid False -gid False -boot config.boot
    """
    loader = lambda: utils.import_settings(True, from_dir=os.getcwd(), boot_module=boot)
    utils.start_server(pid, FORCE, chroot, chdir, uid, gid, umask, loader)


def stop_command(pid='./run/smtp.pid', KILL=False, ALL=False):
    """
    Stops a running lamson server.  Give -KILL True to have it
    stopped violently.  The PID file is removed after the 
    signal is sent.  Give -ALL the name of a run directory and
    it will stop all pid files it finds there.

    lamson stop -pid ./run/smtp.pid -KILL False -ALL False
    """
    pid_files = []

    if ALL:
        pid_files = glob.glob(ALL + "/*.pid")
    else:
        pid_files = [pid]

        if not os.path.exists(pid):
            print "PID file %s doesn't exist, maybe Lamson isn't running?" % pid
            sys.exit(1)
            return # for unit tests mocking sys.exit

    print "Stopping processes with the following PID files: %s" % pid_files

    for pid_f in pid_files:
        pid = open(pid_f).readline()

        print "Attempting to stop lamson at pid %d" % int(pid)

        try:
            if KILL:
                os.kill(int(pid), signal.SIGKILL)
            else:
                os.kill(int(pid), signal.SIGHUP)
            
            os.unlink(pid_f)
            os.unlink(pid_f + ".lock")
        except OSError, exc:
            print "ERROR stopping Lamson on PID %d: %s" % (int(pid), exc)


def restart_command(**options):
    """
    Simply attempts a stop and then a start command.  All options for both
    apply to restart.  See stop and start for options available.
    """

    stop_command(**options)
    time.sleep(2)
    start_command(**options)


def status_command(pid='./run/smtp.pid'):
    """
    Prints out status information about lamson useful for finding out if it's
    running and where.

    lamson status -pid ./run/smtp.pid
    """
    if os.path.exists(pid):
        pid = open(pid).readline()
        print "Lamson running with PID %d" % int(pid)
    else:
        print "Lamson not running."


def help_command(**options):
    """
    Prints out help for the commands. 

    lamson help

    You can get help for one command with:

    lamson help -for STR
    """
    if "for" in options:
        help_text = args.help_for_command(lamson.commands, options['for'])
        if help_text:
            print help_text
        else:
            args.invalid_command_message(lamson.commands, exit_on_error=True)
    else:
        print "Available commands:\n"
        print ", ".join(args.available_commands(lamson.commands))
        print "\nUse lamson help -for <command> to find out more."


def queue_command(pop=False, get=False, keys=False, remove=False, count=False,
                  clear=False, name="run/queue"):
    """
    Let's you do most of the operations available to a queue.

    lamson queue (-pop | -get | -remove | -count | -clear | -keys) -name run/queue
    """
    print "Using queue: %r" % name

    inq = queue.Queue(name)

    if pop:
        key, msg = inq.pop()
        if key:
            print "KEY: ", key
            print msg
    elif get:
        print inq.get(get)
    elif remove:
        inq.remove(remove)
    elif count:
        print "Queue %s contains %d messages" % (name, inq.count())
    elif clear:
        inq.clear()
    elif keys:
        print "\n".join(inq.keys())
    else:
        print "Give something to do.  Try lamson help -for queue to find out what."
        sys.exit(1)
        return # for unit tests mocking sys.exit
        

def routes_command(TRAILING=['config.testing'], path=os.getcwd(), test=""):
    """
    Prints out valuable information about an application's routing configuration
    after everything is loaded and ready to go.  Helps debug problems with
    messages not getting to your handlers.  Path has the search paths you want
    separated by a ':' character, and it's added to the sys.path.

    lamson routes -path $PWD -- config.testing -test ""

    It defaults to running your config.testing to load the routes. 
    If you want it to run the config.boot then give that instead:

    lamson routes -- config.boot

    You can also test a potential target by doing -test EMAIL.

    """
    modules = TRAILING
    sys.path += path.split(':')
    test_case_matches = []

    for module in modules:
        __import__(module, globals(), locals())

    print "Routing ORDER: ", routing.Router.ORDER
    print "Routing TABLE: \n---"
    for format in routing.Router.REGISTERED:
        print "%r: " % format,
        regex, functions = routing.Router.REGISTERED[format]
        for func in functions:
            print "%s.%s " % (func.__module__, func.__name__),
            match = regex.match(test)
            if test and match:
                test_case_matches.append((format, func, match))

        print "\n---"

    if test_case_matches:
        print "\nTEST address %r matches:" % test
        for format, func, match in test_case_matches:
            print "  %r %s.%s" % (format, func.__module__, func.__name__)
            print "  -  %r" % (match.groupdict())
    elif test:
        print "\nTEST address %r didn't match anything." % test



def gen_command(project=None, FORCE=False):
    """
    Generates various useful things for you to get you started.

    lamson gen -project STR -FORCE False
    """
    project = project

    if os.path.exists(project) and not FORCE:
        print "Project %s exists, delete it first." % project
        sys.exit(1)
        return

    prototype = ZipFile(resource_stream(__name__, 'data/prototype.zip'))
    # looks like the very handy ZipFile.extractall is only in python 2.6

    if not os.path.exists(project):
        os.makedirs(project)

    files = prototype.namelist()

    for gen_f in files:
        if str(gen_f).endswith('/'):
            target = os.path.join(project, gen_f)
            if not os.path.exists(target):
                print "mkdir: %s" % target
                os.makedirs(target)
        else:
            target = os.path.join(project, gen_f)
            if os.path.exists(target): 
                continue

            print "copy: %s" % target
            out = open(target, 'w')
            out.write(prototype.read(gen_f))
            out.close()


def web_command(basedir=".", port=8888, host='127.0.0.1'):
    """
    Starts a very simple files only web server for easy testing of applications
    that need to make some HTML files as the result of their operation.
    If you need more than this then use a real web server.

    lamson web -basedir "." -port 8888 -host '127.0.0.1'

    This command doesn't exit so you can view the logs it prints out.
    """
    from BaseHTTPServer import HTTPServer
    from SimpleHTTPServer import SimpleHTTPRequestHandler 

    os.chdir(basedir)
    web = HTTPServer((host, port), SimpleHTTPRequestHandler)
    print "Starting server on %s:%d out of directory %r" % (
        host, port, basedir)
    web.serve_forever()


def cleanse_command(input=None, output=None):
    """
    Uses Lamson mail cleansing and canonicalization system to take an
    input maildir (or mbox) and replicate the email over into another
    maildir.  It's used mostly for testing and cleaning.
    """
    error_count = 0

    try:
        inbox = mailbox.mbox(input)
    except:
        inbox = mailbox.Maildir(input, factory=None)

    outbox = mailbox.Maildir(output)

    for msg in inbox:
        try:
            mail = encoding.from_message(msg)
            outbox.add(encoding.to_string(mail))
        except encoding.EncodingError, exc:
            print "ERROR: ", exc
            error_count += 1

    outbox.close()
    inbox.close()

    print "TOTAL ERRORS:", error_count


def blast_command(input=None, host='127.0.0.1', port=8823, debug=0):
    """
    Given a maildir, this command will go through each email
    and blast it at your server.  It does nothing to the message, so
    it will be real messages hitting your server, not cleansed ones.
    """
    inbox = mailbox.Maildir(input)
    relay = server.Relay(host, port=port, debug=debug)

    for key in inbox.keys():
        msgfile = inbox.get_file(key)
        msg = email.message_from_file(msgfile)
        relay.deliver(msg)


def version_command():
    """
    Prints the version of Lamson, the reporitory revision, and the
    file it came from.
    """

    from lamson import version

    print "Lamson-Version: ", version.VERSION['version']
    print "Repository-Revision:", version.VERSION['rev'][0]
    print "Repository-Hash:", version.VERSION['rev'][1]
    print "Version-File:", version.__file__
    print ""
    print "Lamson is Copyright (C) Zed A. Shaw 2008-2009.  Licensed GPLv3."
    print "If you didn't get a copy of the LICENSE contact the author at:\n"
    print "   zedshaw@zedshaw.com"
    print ""
    print "Have fun."

��������������������������������������������������������������������������lamson-1.0pre11/lamson/confirm.py�������������������������������������������������������������������0000644�0000765�0000024�00000016574�11310765311�016357� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Confirmation handling API that helps you get the whole confirm/pending/verify 
process correct.  It doesn't implement any handlers, but what it does do is
provide the logic for doing the following:

    * Take an email, put it in a "pending" queue, and then send out a confirm
    email with a strong random id.
    * Store the pending message ID and the random secret someplace for later
    verification.
    * Verify an incoming email against the expected ID, and get back the
    original.

You then just work this into your project's state flow, write your own
templates, and possibly write your own storage.
"""

import uuid
from lamson import queue, view
from email.utils import parseaddr

class ConfirmationStorage(object):
    """
    This is the basic confirmation storage.  For simple testing purposes
    you can just use the default hash db parameter.  If you do a deployment
    you can probably get away with a shelf hash instead.

    You can write your own version of this and use it.  The confirmation engine
    only cares that it gets something that supports all of these methods.
    """
    def __init__(self, db={}):
        """
        Change the db parameter to a shelf to get persistent storage.
        """
        self.confirmations = db

    def clear(self):
        """
        Used primarily in testing, this clears out all pending confirmations.
        """
        self.confirmations.clear()

    def key(self, target, from_address):
        """
        Used internally to construct a string key, if you write
        your own you don't need this.

        NOTE: To support proper equality and shelve storage, this encodes the
        key into ASCII.  Make a different subclass if you need unicode and your
        storage supports it.
        """
        key = target + ':' + from_address

        return key.encode('ascii')

    def get(self, target, from_address):
        """
        Given a target and a from address, this returns a tuple of (expected_secret, pending_message_id).
        If it doesn't find that target+from_address, then it should return a (None, None) tuple.
        """
        return self.confirmations.get(self.key(target, from_address), (None, None))

    def delete(self, target, from_address):
        """
        Removes a target+from_address from the storage.
        """
        try:
            del self.confirmations[self.key(target, from_address)]
        except KeyError:
            pass

    def store(self, target, from_address, expected_secret, pending_message_id):
        """
        Given a target, from_address it will store the expected_secret and pending_message_id
        of later verification.  The target should be a string indicating what is being
        confirmed.  Like "subscribe", "post", etc.

        When implementing your own you should *never* allow more than one target+from_address
        combination.
        """
        self.confirmations[self.key(target, from_address)] = (expected_secret,
                                                              pending_message_id)

class ConfirmationEngine(object):
    """
    The confirmation engine is what does the work of sending a confirmation, 
    and verifying that it was confirmed properly.  In order to use it you
    have to construct the ConfirmationEngine (usually in config/settings.py) and
    you write your confirmation message templates for sending.

    The primary methods you use are ConfirmationEngine.send and ConfirmationEngine.verify.
    """
    def __init__(self, pending_queue, storage):
        """
        The pending_queue should be a string with the path to the lamson.queue.Queue 
        that will store pending messages.  These messages are the originals the user
        sent when they tried to confirm.

        Storage should be something that is like ConfirmationStorage so that this
        can store things for later verification.
        """
        self.pending = queue.Queue(pending_queue)
        self.storage = storage

    def get_pending(self, pending_id):
        """
        Returns the pending message for the given ID.
        """
        return self.pending.get(pending_id)

    def push_pending(self, message):
        """
        Puts a pending message into the pending queue.
        """
        return self.pending.push(message)

    def delete_pending(self, pending_id):
        """
        Removes the pending message from the pending queue.
        """
        self.pending.remove(pending_id)


    def cancel(self, target, from_address, expect_secret):
        """
        Used to cancel a pending confirmation.
        """
        name, addr = parseaddr(from_address)

        secret, pending_id = self.storage.get(target, addr)

        if secret == expect_secret:
            self.storage.delete(target, addr)
            self.delete_pending(pending_id)

    def make_random_secret(self):
        """
        Generates a random uuid as the secret, in hex form.
        """
        return uuid.uuid4().hex

    def register(self, target, message):
        """
        Don't call this directly unless you know what you are doing.
        It does the job of registering the original message and the
        expected confirmation into the storage.
        """
        from_address = message.route_from

        pending_id = self.push_pending(message)
        secret = self.make_random_secret()
        self.storage.store(target, from_address, secret, pending_id)

        return "%s-confirm-%s" % (target, secret)

    def verify(self, target, from_address, expect_secret):
        """
        Given a target (i.e. "subscribe", "post", etc), a from_address
        of someone trying to confirm, and the secret they should use, this
        will try to verify their confirmation.  If the verify works then
        you'll get the original message back to do what you want with.

        If the verification fails then you are given None.

        The message is *not* deleted from the pending queue.  You can do
        that yourself with delete_pending.
        """
        assert expect_secret, "Must give an expected ID number."
        name, addr = parseaddr(from_address)

        secret, pending_id = self.storage.get(target, addr)

        if secret == expect_secret:
            self.storage.delete(target, addr)
            return self.get_pending(pending_id)
        else:
            return None

    def send(self, relay, target, message, template, vars):
        """
        This is the method you should use to send out confirmation messages.
        You give it the relay, a target (i.e. "subscribe"), the message they
        sent requesting the confirm, your confirmation template, and any
        vars that template needs.

        The result of calling this is that the template message gets sent through
        the relay, the original message is stored in the pending queue, and 
        data is put into the storage for later calls to verify.
        """
        confirm_address = self.register(target, message)
        vars.update(locals())
        msg = view.respond(vars, template, To=message['from'],
                           From="%(confirm_address)s@%(host)s",
                           Subject="Confirmation required")

        msg['Reply-To'] = "%(confirm_address)s@%(host)s" % vars

        relay.deliver(msg)

    def clear(self):
        """
        Used in testing to make sure there's nothing in the pending
        queue or storage.
        """
        self.pending.clear()
        self.storage.clear()
������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/������������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�015256� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/��������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�017323� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/����������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020103� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/__init__.py�����������������������������������������������0000644�0000765�0000024�00000000000�11206704215�022170� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/handlers/�������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021703� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/handlers/__init__.py��������������������������������������0000644�0000765�0000024�00000000000�11206704215�023770� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/handlers/sample.py����������������������������������������0000644�0000765�0000024�00000001035�11227341233�023523� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import logging
from lamson.routing import route, route_like, stateless
from config.settings import relay
from lamson import view


@route("(address)@(host)", address=".+")
def START(message, address=None, host=None):
    return NEW_USER


@route_like(START)
def NEW_USER(message, address=None, host=None):
    return NEW_USER


@route_like(START)
def END(message, address=None, host=None):
    return NEW_USER(message, address, host)


@route_like(START)
@stateless
def FORWARD(message, address=None, host=None):
    relay.deliver(message)

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/model/����������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021203� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/app/model/__init__.py�����������������������������������������0000644�0000765�0000024�00000000000�11206704215�023270� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�020570� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/__init__.py��������������������������������������������0000644�0000765�0000024�00000000000�11206704215�022655� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/boot.py������������������������������������������������0000644�0000765�0000024�00000001635�11313446604�022104� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson.routing import Router
from lamson.server import Relay, SMTPReceiver
from lamson import view, queue
import logging
import logging.config
import jinja2

logging.config.fileConfig("config/logging.conf")

# the relay host to actually send the final message to
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=1)

# where to listen for incoming messages
settings.receiver = SMTPReceiver(settings.receiver_config['host'],
                                 settings.receiver_config['port'])

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.UNDELIVERABLE_QUEUE=queue.Queue("run/undeliverable")

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

���������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/logging.conf�������������������������������������������0000644�0000765�0000024�00000001064�11212227442�023053� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=fileHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[logger_routing]
level=DEBUG
handlers=fileHandler
qualname=routing
propagate=0

[handler_fileHandler]
# this works using FileHandler
class=FileHandler
# If you have Python2.6 you can use this and it will work when you use logrotate
#class=WatchedFileHandler
level=DEBUG
formatter=defaultFormatter
args=("logs/lamson.log",)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/settings.py��������������������������������������������0000644�0000765�0000024�00000001117�11313447221�022770� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file contains python variables that configure Lamson for email processing.
import logging

# You may add additional parameters such as `username' and `password' if your
# relay server requires authentication, `starttls' (boolean) or `ssl' (boolean)
# for secure connections.
relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

handlers = ['app.handlers.sample']

router_defaults = {'host': '.+'}

template_config = {'dir': 'app', 'module': 'templates'}

# the config/boot.py will turn these values into variables set in settings
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/test_logging.conf��������������������������������������0000644�0000765�0000024�00000001042�11212227411�024102� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=stdoutHandler,stderrHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=stdoutHandler

[logger_routing]
level=DEBUG
handlers=stderrHandler
qualname=routing
propagate=0

[handler_stdoutHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)

[handler_stderrHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stderr,)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/config/testing.py���������������������������������������������0000644�0000765�0000024�00000002103�11227341247�022606� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson import view
from lamson.routing import Router
from lamson.server import Relay
import jinja2
import logging
import logging.config
import os

logging.config.fileConfig("config/test_logging.conf")

# the relay host to actually send the final message to (set debug=1 to see what
# the relay is saying to the log server).
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=0)


settings.receiver = None

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers)
Router.RELOAD=True
Router.LOG_EXCEPTIONS=False

view.LOADER = jinja2.Environment(
    loader=jinja2.PackageLoader(settings.template_config['dir'], 
                                settings.template_config['module']))

# if you have pyenchant and enchant installed then the template tests will do
# spell checking for you, but you need to tell pyenchant where to find itself
# if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
#     os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/muttrc��������������������������������������������������������0000644�0000765�0000024�00000000345�11211037063�020550� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������set mbox_type=Maildir
set folder="run/queue"
set mask="!^\\.[^.]"
set mbox="run/queue"
set record="+.Sent"
set postponed="+.Drafts"
set spoolfile="run/queue"
set sendmail="/usr/bin/env lamson sendmail -port 8823 -host 127.0.0.1"
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/README��������������������������������������������������������0000644�0000765�0000024�00000000051�11206704215�020165� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������This is your initial Lamson application.
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/��������������������������������������������������������0000755�0000765�0000024�00000000000�11313464573�020464� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/handlers/�����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022265� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/handlers/__init__.py������������������������������������0000644�0000765�0000024�00000000000�11206704215�024352� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/handlers/open_relay_tests.py����������������������������0000644�0000765�0000024�00000001701�11313450613�026202� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.testing import *
import os
from lamson import server

relay = relay(port=8823)
client = RouterConversation("somedude@localhost", "requests_tests")
confirm_format = "testing-confirm-[0-9]+@"
noreply_format = "testing-noreply@"


def test_forwards_relay_host():
    """
    Makes sure that your config/settings.py is configured to forward mail from
    localhost (or your direct host) to your relay.
    """
    client.begin()
    client.say("tester@localhost", "Test that forward works.", "tester@localhost")


def test_drops_open_relay_messages():
    """
    But, make sure that mail NOT for test.com gets dropped silently.
    """
    client.begin()
    client.say("tester@badplace.notinterwebs", "Relay should not happen")
    assert queue().count() == 0, "You are configured currently to accept everything.  You should change config/settings.py router_defaults so host is your actual host name that will receive mail."

���������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/model/��������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�021565� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/model/__init__.py���������������������������������������0000644�0000765�0000024�00000000000�11206704215�023652� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/templates/����������������������������������������������0000755�0000765�0000024�00000000000�11313464574�022463� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype/tests/templates/__init__.py�����������������������������������0000644�0000765�0000024�00000000000�11206704215�024550� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/data/prototype.zip�����������������������������������������������������������0000644�0000765�0000024�00000014722�11313464551�020050� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PK
�����*:��������������app/UT	�^
Jii.KUx��PK
�����:��������������app/__init__.pyUT	�JMi.KUx��PK
�����*:������������	��app/data/UT	�]^
Jii.KUx��PK
�����i(:������������
��app/handlers/UT	�]Jii.KUx��PK
�����:��������������app/handlers/__init__.pyUT	�JMi.KUx��PK����Z&:P0&�������app/handlers/sample.pyUT	�]J]i.KUx��
 }
ٕR`傭vfP[*"o</ѝqXj΅5ÒuVadhaZc$X|{Zcnه$^!H4hB:]YF,۴ؒ΋s wm,aQ8ϪzWf#k>Q:Moࠟ*h)z0'�PK
�����i(:������������
��app/model/UT	�]Jii.KUx��PK
�����:��������������app/model/__init__.pyUT	�JMi.KUx��PK
�����):��������������app/templates/UT	�%\
Jii.KUx��PK
�����B;��������������config/UT	�uO.Kii.KUx��PK
�����:��������������config/__init__.pyUT	�JMi.KUx��PK����A;/������config/boot.pyUT	�M.K]i.KUx��}Ms0=9ʡ3Ut:NcCboB@Dks`Ⱦnl6@	g)\($A)reVkl<ق8$�Ge9!Rvݍ$p
:Dy.cl;vj
Oկs7;C[.RDiL\)*@JxMSBϾ_#䡿ȭ\6:+eAHet%RsPvz
onp
HB5UۤJZ7ҠRӖnYJAr&p)̨ԡ4=Dqh,{Bf)P7ؙl(6qD$Xa-qm;-bOS"{^U(x)_PK����w=:��4����config/logging.confUT	�"/)J]i.KUx��MO0֪I4-n\d{vѸp׏_;<gjKk,륵Rmiժ5>BrCk
{#s>h&z~fО~y}zSg
Gc#}6n5
yԒioz]Afg`p=4'qq4
A28ƌx84dGQN5L3QxT/f
v*gE/m[vBy\Hтh ;b:DΫNPK����oB;2W��O����config/settings.pyUT	�N.K]i.KUx��RJAWzŲ"?EDt7
NInZG˒d2dIa>(	lDw%<㤞$M,rڵ
Od(MW)0ƒIq"6E)'G`a3Ar8Jɮ)){Q8XldaU_F
Y�W[Hݨr*ms1
+'Q:`	zuqNGsvƺ)_պK4#W1Z4lsYNWs\yo}3G8p`%TP2|PK����k=:]q���"����config/test_logging.confUT	�	/)J]i.KUx��0E|R
 ^;bBUeQ7 l'A[| q.`MKLsa:lWeBi[A!Q9@gCieOMK_DYbQ-iwT;nr+PA`@PϻP途fBp>;agx2tfV?',=rv8t4^i2	Je\PK����b&:	<
��C����config/testing.pyUT	�]J]i.KUx��}SMo0WNQ8ێ|H3o-$AC"0R 	'q>e|O#\]CU!JN Lj5`{|dtC_â٣9P>!,}¨7de
!qmû>5vh]zI1vB0$�*m8
<s
M%%M"¡`yKJˡ|aQO)$"Ҿ`hFg{_|
21̵Br,x#
Jh4pyxbU\^%G6
WNe1Lqӛ!J^jT.욧GxɳO0.yI4\nWuH6:V7P=®Eaev6R	@~pRB}5C*>36TQHevP<Mpeko|<jzKs�J2D?@yux_%Kq9bKQM>PK
�����B;��������������logs/UT	�tO.Kii.KUx��PK����m:)_N@��������muttrcUT	�3>$J]i.KUx��eα0ݧ]
Tp)rDb۫wۀq!}cµO�}4Zqm%^=GAN?\7u԰$!uZn8[@=L| G,,j|_ZIW_RUUDvOWDQmBPK
�����:
)���)�����READMEUT	�J]i.KUx��This is your initial Lamson application.
PK
�����*:��������������run/UT	�i^
Jii.KUx��PK
�����*:��������������tests/UT	�^
Jii.KUx��PK
�����D;��������������tests/handlers/UT	�Q.Kii.KUx��PK
�����:��������������tests/handlers/__init__.pyUT	�JMi.KUx��PK����D;,?����"��tests/handlers/open_relay_tests.pyUT	�Q.K]i.KUx��Rn0+F9%
\
J=Pu&YkOڿgMa=77o4C .y7/Tc.x3.D?,OU	P&77?C|GADGfҀ{X_A3xGdzY+כ?U ş^i]+SUv4<H_tYA~@yo( ^)Oʻ;[NdEsHrll]=)4g1lA%g+N.4%$ye?9˵KgEzZ0(b&x_>W]g59F̻A*57@j"GI顤PAWO>3Mk
W¦UI)akb61y+Z\" Y_{&|-\߫&yu@:lؘ?c̫G罆{gPK
�����*:��������������tests/model/UT	�^
Jii.KUx��PK
�����:��������������tests/model/__init__.pyUT	�JMi.KUx��PK
�����*:��������������tests/templates/UT	�^
Jii.KUx��PK
�����:��������������tests/templates/__init__.pyUT	�JMi.KUx��PK
�����*:�������������
��������A����app/UT�^
JUx��PK
�����:�������������
���������7���app/__init__.pyUT�JUx��PK
�����*:������������	�
��������Ay���app/data/UT�]^
JUx��PK
�����i(:������������
�
��������A���app/handlers/UT�]JUx��PK
�����:�������������
������������app/handlers/__init__.pyUT�JUx��PK����Z&:P0&������
��������@��app/handlers/sample.pyUT�]JUx��PK
�����i(:������������
�
��������A^��app/model/UT�]JUx��PK
�����:�������������
�����������app/model/__init__.pyUT�JUx��PK
�����):�������������
��������A��app/templates/UT�%\
JUx��PK
�����B;�������������
��������A$��config/UT�uO.KUx��PK
�����:�������������
���������^��config/__init__.pyUT�JUx��PK����A;/�����
����������config/boot.pyUT�M.KUx��PK����w=:��4���
��������w��config/logging.confUT�"/)JUx��PK����oB;2W��O���
����������config/settings.pyUT�N.KUx��PK����k=:]q���"���
��������p��config/test_logging.confUT�	/)JUx��PK����b&:	<
��C���
��������	��config/testing.pyUT�]JUx��PK
�����B;�������������
��������A��logs/UT�tO.KUx��PK����m:)_N@�������
����������muttrcUT�3>$JUx��PK
�����:
)���)����
����������READMEUT�JUx��PK
�����*:�������������
��������AO
��run/UT�i^
JUx��PK
�����*:�������������
��������A
��tests/UT�^
JUx��PK
�����D;�������������
��������A
��tests/handlers/UT�Q.KUx��PK
�����:�������������
�����������tests/handlers/__init__.pyUT�JUx��PK����D;,?����"�
��������N��tests/handlers/open_relay_tests.pyUT�Q.KUx��PK
�����*:�������������
��������A��tests/model/UT�^
JUx��PK
�����:�������������
�����������tests/model/__init__.pyUT�JUx��PK
�����*:�������������
��������A
��tests/templates/UT�^
JUx��PK
�����:�������������
���������M��tests/templates/__init__.pyUT�JUx��PK������!����������������������������������������������������lamson-1.0pre11/lamson/encoding.py������������������������������������������������������������������0000644�0000765�0000024�00000042523�11311252544�016501� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Lamson takes the policy that email it receives is most likely complete garbage 
using bizarre pre-Unicode formats that are irrelevant and unnecessary in today's
modern world.  These emails must be cleansed of their unholy stench of
randomness and turned into something nice and clean that a regular Python
programmer can work with:  unicode.

That's the receiving end, but on the sending end Lamson wants to make the world
better by not increasing the suffering.  To that end, Lamson will canonicalize
all email it sends to be ascii or utf-8 (whichever is simpler and works to
encode the data).  When you get an email from Lamson, it is a pristine easily
parseable clean unit of goodness you can count on.

To accomplish these tasks, Lamson goes back to basics and assert a few simple
rules on each email it receives:

1) NO ENCODING IS TRUSTED, NO LANGUAGE IS SACRED, ALL ARE SUSPECT.
2) Python wants Unicode, it will get Unicode.
3) Any email that CANNOT become Unicode, CANNOT be processed by Lamson or
Python.
4) Email addresses are ESSENTIAL to Lamson's routing and security, and therefore
will be canonicalized and properly encoded.
5) Lamson will therefore try to "upgrade" all email it receives to Unicode
internally, and cleaning all email addresses.
6) It does this by decoding all codecs, and if the codec LIES, then it will
attempt to statistically detect the codec using chardet.
7) If it can't detect the codec, and the codec lies, then the email is bad.
8) All text bodies and attachments are then converted to Python unicode in the
same way as the headers.
9) All other attachments are converted to raw strings as-is.

Once Lamson has done this, your Python handler can now assume that all
MailRequest objects are happily unicode enabled and ready to go.  The rule is:

    IF IT CANNOT BE UNICODE, THEN PYTHON CANNOT WORK WITH IT.

On the outgoing end (when you send a MailResponse), Lamson tries to create the
email it wants to receive by canonicalizing it:

1) All email will be encoded in the simplest cleanest way possible without
losing information.
2) All headers are converted to 'ascii', and if that doesn't work, then 'utf-8'.
3) All text/* attachments and bodies are converted to ascii, and if that doesn't
work, 'utf-8'.
4) All other attachments are left alone.
5) All email addresses are normalized and encoded if they have not been already.

The end result is an email that has the highest probability of not containing
any obfuscation techniques, hidden characters, bad characters, improper
formatting, invalid non-characterset headers, or any of the other billions of
things email clients do to the world.  The output rule of Lamson is:

    ALL EMAIL IS ASCII FIRST, THEN UTF-8, AND IF CANNOT BE EITHER THOSE IT WILL
    NOT BE SENT.

Following these simple rules, this module does the work of converting email
to the canonical format and sending the canonical format.  The code is 
probably the most complex part of Lamson since the job it does is difficult.

Test results show that Lamson can safely canonicalize most email from any
culture (not just English) to the canonical form, and that if it can't then the
email is not formatted right and/or spam.

If you find an instance where this is not the case, then submit it to the
project as a test case.
"""

import string
from email.charset import Charset
import chardet
import re
import email
from email import encoders
from email.mime.base import MIMEBase
from email.utils import parseaddr
import sys


DEFAULT_ENCODING = "utf-8"
DEFAULT_ERROR_HANDLING = "strict"
CONTENT_ENCODING_KEYS = set(['Content-Type', 'Content-Transfer-Encoding',
                             'Content-Disposition', 'Mime-Version'])
CONTENT_ENCODING_REMOVED_PARAMS = ['boundary']

REGEX_OPTS = re.IGNORECASE | re.MULTILINE
ENCODING_REGEX = re.compile(r"\=\?([a-z0-9\-]+?)\?([bq])\?", REGEX_OPTS)
ENCODING_END_REGEX = re.compile(r"\?=", REGEX_OPTS)
INDENT_REGEX = re.compile(r"\n\s+")

VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc', 'Bcc']

class EncodingError(Exception): 
    """Thrown when there is an encoding error."""
    pass


class MailBase(object):
    """MailBase is used as the basis of lamson.mail and contains the basics of
    encoding an email.  You actually can do all your email processing with this
    class, but it's more raw.
    """
    def __init__(self, items=()):
        self.headers = dict(items)
        self.parts = []
        self.body = None
        self.content_encoding = {'Content-Type': (None, {}), 
                                 'Content-Disposition': (None, {}),
                                 'Content-Transfer-Encoding': (None, {})}

    def __getitem__(self, key):
        return self.headers.get(normalize_header(key), None)

    def __len__(self):
        return len(self.headers)

    def __iter__(self):
        return iter(self.headers)

    def __contains__(self, key):
        return normalize_header(key) in self.headers

    def __setitem__(self, key, value):
        self.headers[normalize_header(key)] = value

    def __delitem__(self, key):
        del self.headers[normalize_header(key)]

    def __nonzero__(self):
        return self.body != None or len(self.headers) > 0 or len(self.parts) > 0

    def keys(self):
        """Returns the sorted keys."""
        return sorted(self.headers.keys())

    def attach_file(self, filename, data, ctype, disposition):
        """
        A file attachment is a raw attachment with a disposition that
        indicates the file name.
        """
        assert filename, "You can't attach a file without a filename."
        assert ctype.lower() == ctype, "Hey, don't be an ass.  Use a lowercase content type."

        part = MailBase()
        part.body = data
        part.content_encoding['Content-Type'] = (ctype, {'name': filename})
        part.content_encoding['Content-Disposition'] = (disposition,
                                                        {'filename': filename})
        self.parts.append(part)


    def attach_text(self, data, ctype):
        """
        This attaches a simpler text encoded part, which doesn't have a
        filename.
        """
        assert ctype.lower() == ctype, "Hey, don't be an ass.  Use a lowercase content type."

        part = MailBase()
        part.body = data
        part.content_encoding['Content-Type'] = (ctype, {})
        self.parts.append(part)

    def walk(self):
        for p in self.parts:
            yield p
            for x in p.walk():
                yield x


class MIMEPart(MIMEBase):
    """
    A reimplementation of nearly everything in email.mime to be more useful
    for actually attaching things.  Rather than one class for every type of
    thing you'd encode, there's just this one, and it figures out how to
    encode what you ask it.
    """
    def __init__(self, type, **params):
        self.maintype, self.subtype = type.split('/')
        MIMEBase.__init__(self, self.maintype, self.subtype, **params)

    def add_text(self, content):
        # this is text, so encode it in canonical form
        try:
            encoded = content.encode('ascii')
            charset = 'ascii'
        except UnicodeError:
            encoded = content.encode('utf-8')
            charset = 'utf-8'

        self.set_payload(encoded, charset=charset)


    def extract_payload(self, mail):
        if mail.body == None: return  # only None, '' is still ok

        ctype, ctype_params = mail.content_encoding['Content-Type']
        cdisp, cdisp_params = mail.content_encoding['Content-Disposition']

        assert ctype, "Extract payload requires that mail.content_encoding have a valid Content-Type."

        if ctype.startswith("text/"):
            self.add_text(mail.body)
        else:
            if cdisp:
                # replicate the content-disposition settings
                self.add_header('Content-Disposition', cdisp, **cdisp_params)

            self.set_payload(mail.body)
            encoders.encode_base64(self)

    def __repr__(self):
        return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (self.subtype, self.maintype, self['Content-Type'],
                                              self['Content-Disposition'],
                                                            self.is_multipart())

def from_message(message):
    """
    Given a MIMEBase or similar Python email API message object, this
    will canonicalize it and give you back a pristine MailBase.
    If it can't then it raises a EncodingError.
    """
    mail = MailBase()

    # parse the content information out of message
    for k in CONTENT_ENCODING_KEYS:
        setting, params = parse_parameter_header(message, k)
        setting = setting.lower() if setting else setting
        mail.content_encoding[k] = (setting, params)

    # copy over any keys that are not part of the content information
    for k in message.keys():
        if normalize_header(k) not in mail.content_encoding:
            mail[k] = header_from_mime_encoding(message[k])
  
    decode_message_body(mail, message)

    if message.is_multipart():
        # recursively go through each subpart and decode in the same way
        for msg in message.get_payload():
            if msg != message:  # skip the multipart message itself
                mail.parts.append(from_message(msg))

    return mail



def to_message(mail):
    """
    Given a MailBase message, this will construct a MIMEPart 
    that is canonicalized for use with the Python email API.
    """
    ctype, params = mail.content_encoding['Content-Type']

    if not ctype:
        if mail.parts:
            ctype = 'multipart/mixed'
        else:
            ctype = 'text/plain'
    else:
        if mail.parts:
            assert ctype.startswith("multipart") or ctype.startswith("message"), "Content type should be multipart or message, not %r" % ctype

    # adjust the content type according to what it should be now
    mail.content_encoding['Content-Type'] = (ctype, params)

    try:
        out = MIMEPart(ctype, **params)
    except TypeError, exc:
        raise EncodingError("Content-Type malformed, not allowed: %r; %r (Python ERROR: %s" %
                            (ctype, params, exc.message))

    for k in mail.keys():
        if k in ADDRESS_HEADERS_WHITELIST:
            out[k.encode('ascii')] = header_to_mime_encoding(mail[k])
        else:
            out[k.encode('ascii')] = header_to_mime_encoding(mail[k], not_email=True)

    out.extract_payload(mail)

    # go through the children
    for part in mail.parts:
        out.attach(to_message(part))

    return out


def to_string(mail, envelope_header=False):
    """Returns a canonicalized email string you can use to send or store
    somewhere."""
    msg = to_message(mail).as_string(envelope_header)
    assert "From nobody" not in msg
    return msg


def from_string(data):
    """Takes a string, and tries to clean it up into a clean MailBase."""
    return from_message(email.message_from_string(data))


def to_file(mail, fileobj):
    """Writes a canonicalized message to the given file."""
    fileobj.write(to_string(mail))

def from_file(fileobj):
    """Reads an email and cleans it up to make a MailBase."""
    return from_message(email.message_from_file(fileobj))


def normalize_header(header):
    return string.capwords(header.lower(), '-')


def parse_parameter_header(message, header):
    params = message.get_params(header=header)
    if params:
        value = params.pop(0)[0]
        params_dict = dict(params)

        for key in CONTENT_ENCODING_REMOVED_PARAMS:
            if key in params_dict: del params_dict[key]

        return value, params_dict
    else:
        return None, {}

def decode_message_body(mail, message):
    mail.body = message.get_payload(decode=True)
    if mail.body:
        # decode the payload according to the charset given if it's text
        ctype, params = mail.content_encoding['Content-Type']

        if not ctype:
            charset = 'ascii'
            mail.body = attempt_decoding(charset, mail.body)
        elif ctype.startswith("text/"):
            charset = params.get('charset', 'ascii')
            mail.body = attempt_decoding(charset, mail.body)
        else:
            # it's a binary codec of some kind, so just decode and leave it
            # alone for now
            pass


def properly_encode_header(value, encoder, not_email):
    """
    The only thing special (weird) about this function is that it tries
    to do a fast check to see if the header value has an email address in
    it.  Since random headers could have an email address, and email addresses
    have weird special formatting rules, we have to check for it.

    Normally this works fine, but in Librelist, we need to "obfuscate" email
    addresses by changing the '@' to '-AT-'.  This is where
    VALUE_IS_EMAIL_ADDRESS exists.  It's a simple lambda returning True/False
    to check if a header value has an email address.  If you need to make this
    check different, then change this.
    """
    try:
        return value.encode("ascii")
    except UnicodeEncodeError:
        if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
            # this could have an email address, make sure we don't screw it up
            name, address = parseaddr(value)
            return '"%s" <%s>' % (encoder.header_encode(name.encode("utf-8")), address)

        return encoder.header_encode(value.encode("utf-8"))


def header_to_mime_encoding(value, not_email=False):
    if not value: return ""

    encoder = Charset(DEFAULT_ENCODING)
    if type(value) == list:
        return "; ".join(properly_encode_header(v, encoder, not_email) for v in value)
    else:
        return properly_encode_header(value, encoder, not_email)


def header_from_mime_encoding(header):
    if header is None: 
        return header
    elif type(header) == list:
        return [properly_decode_header(h) for h in header]
    else:
        return properly_decode_header(header)




def guess_encoding_and_decode(original, data, errors=DEFAULT_ERROR_HANDLING):
    try:
        charset = chardet.detect(str(data))

        if not charset['encoding']:
            raise EncodingError("Header claimed %r charset, but detection found none.  Decoding failed." % original)

        return data.decode(charset["encoding"], errors)
    except UnicodeError, exc:
        raise EncodingError("Header lied and claimed %r charset, guessing said "
                            "%r charset, neither worked so this is a bad email: "
                            "%s." % (original, charset, exc))


def attempt_decoding(charset, dec):
    try:
        if isinstance(dec, unicode):
            # it's already unicode so just return it
            return dec
        else:
            return dec.decode(charset)
    except UnicodeError:
        # looks like the charset lies, try to detect it
        return guess_encoding_and_decode(charset, dec)
    except LookupError:
        # they gave a crap encoding
        return guess_encoding_and_decode(charset, dec)


def apply_charset_to_header(charset, encoding, data):
    if encoding == 'b' or encoding == 'B':
        dec = email.base64mime.decode(data.encode('ascii'))
    elif encoding == 'q' or encoding == 'Q':
        dec = email.quoprimime.header_decode(data.encode('ascii'))
    else:
        raise EncodingError("Invalid header encoding %r should be 'Q' or 'B'." % encoding)

    return attempt_decoding(charset, dec)




def _match(data, pattern, pos):
    found = pattern.search(data, pos)
    if found:
        # contract: returns data before the match, and the match groups
        left = data[pos:found.start()]
        return left, found.groups(), found.end()
    else:
        left = data[pos:]
        return left, None, -1



def _tokenize(data, next):
    enc_data = None

    left, enc_header, next = _match(data, ENCODING_REGEX, next)
   
    if next != -1:
        enc_data, _, next = _match(data, ENCODING_END_REGEX, next)

    return left, enc_header, enc_data, next


def _scan(data):
    next = 0
    continued = False
    while next != -1:
        left, enc_header, enc_data, next = _tokenize(data, next)

        if next != -1 and INDENT_REGEX.match(data, next):
            continued = True
        else:
            continued = False

        yield left, enc_header, enc_data, continued


def _parse_charset_header(data):
    scanner = _scan(data)
    oddness = None

    try:
        while True:
            if not oddness:
                left, enc_header, enc_data, continued = scanner.next()
            else:
                left, enc_header, enc_data, continued = oddness
                oddness = None

            while continued:
                l, eh, ed, continued = scanner.next()
               
                if not eh:
                    assert not ed, "Parsing error, give Zed this: %r" % data
                    oddness = (" " + l.lstrip(), eh, ed, continued)
                elif eh[0] == enc_header[0] and eh[1] == enc_header[1]:
                    enc_data += ed
                else:
                    # odd case, it's continued but not from the same base64
                    # need to stack this for the next loop, and drop the \n\s+
                    oddness = ('', eh, ed, continued)
                    break

            if left:
                yield attempt_decoding('ascii', left)
                       
            if enc_header:
                yield apply_charset_to_header(enc_header[0], enc_header[1], enc_data)

    except StopIteration:
        pass


def properly_decode_header(header):
    return u"".join(_parse_charset_header(header))


�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/handlers/��������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016145� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/handlers/__init__.py���������������������������������������������������������0000644�0000765�0000024�00000000367�11210157366�020256� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Lamson comes with a few useful handlers that you can add to your lamson.routing
configuration.  Most of them are implemented as stateless handlers, but some
will require you to read the documentation and configure before you can use
them.
"""
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/handlers/forward.py����������������������������������������������������������0000644�0000765�0000024�00000002112�11212521050�020134� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Implements a forwarding handler that will take anything it receives and
forwards it to the relay host.  It is intended to use with the
lamson.routing.RoutingBase.UNDELIVERABLE_QUEUE if you want mail that Lamson
doesn't understand to be delivered like normal.  The Router will dump
any mail that doesn't match into that queue if you set it, and then you can
load this handler into a special queue receiver to have it forwarded on.

BE VERY CAREFUL WITH THIS.  It should only be used in testing scenarios as
it can turn your server into an open relay if you're not careful.  You
are probably better off writing your own version of this that knows a list
of allowed hosts your machine answers to and only forwards those.
"""

from lamson.routing import route, stateless
from config import settings
import logging

@route("(to)@(host)", to=".+", host=".+")
@stateless
def START(message, to=None, host=None):
    """Forwards every mail it gets to the relay.  BE CAREFULE WITH THIS."""
    logging.debug("MESSAGE to %s@%s forwarded to the relay host.", to, host)
    settings.relay.deliver(message)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/handlers/log.py��������������������������������������������������������������0000644�0000765�0000024�00000001023�11210157530�017257� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Implements a simple logging handler that's actually used by the lamson log
command line tool to run a logging server.  It simply takes every message it
receives and dumps it to the logging.debug stream.
"""

from lamson.routing import route, stateless
import logging

@route("(to)@(host)", to=".+", host=".+")
@stateless
def START(message, to=None, host=None):
    """This is stateless and handles every email no matter what, logging what it receives."""
    logging.debug("MESSAGE to %s@%s:\n%s" % (to, host, str(message)))


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/handlers/queue.py������������������������������������������������������������0000644�0000765�0000024�00000001210�11210366054�017623� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Implements a handler that puts every message it receives into 
the run/queue directory.  It is intended as a debug tool so you
can inspect messages the server is receiving using mutt or 
the lamson queue command.
"""

from lamson.routing import route_like, stateless, nolocking
from lamson import queue, handlers
import logging

@route_like(handlers.log.START)
@stateless
@nolocking
def START(message, to=None, host=None):
    """
    @stateless and routes however handlers.log.START routes (everything).
    Has @nolocking, but that's alright since it's just writing to a maildir.
    """
    q = queue.Queue('run/queue')
    q.push(message)

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/html.py����������������������������������������������������������������������0000644�0000765�0000024�00000014764�11243677626�015705� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
This implements an HTML Mail generator that uses templates and CleverCSS
to produce an HTML message with inline CSS attributes so that it will
display correctly.  As long as you can keep most of the HTML and CSS simple you
should have a high success rate at rendering this.

How it works is you create an HtmlMail class and configure it with a CleverCSS
stylesheet (also a template).  This acts as your template for the appearance and
the outer shell of your HTML.

When you go to send, you use a markdown content template to generate the
guts of your HTML.  You hand this, variables, and email headers to 
HtmlMail.respond and it spits back a fully formed lamson.mail.MailResponse
ready to send.

The engine basically parses the CSS, renders your content template, 
render your outer template, and then applies the CSS directly to your HTML
so your CSS attributes are inline and display in the HTML display.

Each element is a template loaded by your loader: the CleverCSS template, out HTML
template, and your own content.

Finally, use this as a generator by making one and having crank out all the emails
you need.  Don't make one HtmlMail for each message.
"""

from BeautifulSoup import BeautifulSoup
import clevercss
from lamson import mail, view
from markdown2 import markdown


class HtmlMail(object):
    """
    Acts as a lamson.mail.MailResponse generator that produces a properly 
    formatted HTML mail message, including inline CSS applied to all HTML tags.
    """
    def __init__(self, css_template, html_template, variables={}, wiki=markdown):
        """
        You pass in a CleverCSS template (it'll be run through the template engine
        before CleverCSS), the html_template, and any variables that the CSS template
        needs.

        The CSS template is processed once, the html_template is processed each time
        you call render or respond.

        If you don't like markdown, then you can set the wiki variable to any callable
        that processes your templates.
        """
        self.template = html_template
        self.load_css(css_template, variables)
        self.wiki = wiki

    def load_css(self, css_template, variables):
        """
        If you want to change the CSS, simply call this with the new CSS and variables.
        It will change internal state so that later calls to render or respond use
        the new CSS.
        """
        self.css = view.render(variables, css_template)
        self.engine = clevercss.Engine(self.css)
        self.stylesheet = []
        
        for selector, style in self.engine.evaluate():
            attr = "; ".join("%s: %s" % (k,v) for k,v in style)
            selectors = selector[0].split()
            # root, path, attr
            self.stylesheet.append((selectors[0], selectors[1:], attr))


    def reduce_tags(self, name, tags):
        """
        Used mostly internally to find all the tags that fit the given
        CSS selector.  It's fairly primitive, working only on tag names,
        classes, and ids.  You shouldn't get too fancy with the CSS you create.
        """
        results = []

        for tag in tags:
            if name.startswith("#"):
                children = tag.findAll(attrs={"class": name[1:]})
            elif name.startswith("."):
                children = tag.findAll(attrs={"id": name[1:]})
            else:
                children = tag.findAll(name)

            if children:
                results += children

        return results

    def apply_styles(self, html):
        """
        Used mostly internally but helpful for testing, this takes the given HTML
        and applies the configured CSS you've set.  It returns a BeautifulSoup
        object with all the style attributes set and nothing else changed.
        """
        doc = BeautifulSoup(html)
        roots = {}  # the roots rarely change, even though the paths do

        for root, path, attr in self.stylesheet:
            tags = roots.get(root, None)
            
            if not tags:
                tags = self.reduce_tags(root, [doc])
                roots[root] = tags
           
            for sel in path:
                tags = self.reduce_tags(sel, tags)


            for node in tags:
                try:
                    node['style'] += "; " + attr
                except KeyError:
                    node['style'] = attr

        return doc

        
    def render(self, variables, content_template,  pretty=False):
        """
        Works like lamson.view.render, but uses apply_styles to modify
        the HTML with the configured CSS before returning it to you.

        If you set the pretty=True then it will prettyprint the results,
        which is a waste of bandwidth, but helps when debugging.

        Remember that content_template is run through the template system,
        and then processed with self.wiki (defaults to markdown).  This
        let's you do template processing and write the HTML contents like
        you would an email.

        You could also attach the content_template as a text version of the
        message for people without HTML.  Simply set the .Body attribute
        of the returned lamson.mail.MailResponse object.
        """
        content = self.wiki(view.render(variables, content_template))
        lvars = variables.copy()
        lvars['content'] = content

        html = view.render(lvars, self.template)
        styled = self.apply_styles(html)

        if pretty:
            return styled.prettify()
        else:
            return str(styled)


    def respond(self, variables, content, **kwd):
        """
        Works like lamson.view.respond letting you craft a
        lamson.mail.MailResponse immediately from the results of
        a lamson.html.HtmlMail.render call.  Simply pass in the
        From, To, and Subject parameters you would normally pass
        in for MailResponse, and it'll craft the HTML mail for
        you and return it ready to deliver.

        A slight convenience in this function is that if the
        Body kw parameter equals the content parameter, then
        it's assumed you want the raw markdown content to be
        sent as the text version, and it will produce a nice
        dual HTML/text email.
        """
        assert content, "You must give a contents template."

        if kwd.get('Body', None) == content:
            kwd['Body'] = view.render(variables, content)

        for key in kwd:
            kwd[key] = kwd[key] % variables
        
        msg = mail.MailResponse(**kwd)
        msg.Html = self.render(variables, content)

        return msg


    
������������lamson-1.0pre11/lamson/mail.py����������������������������������������������������������������������0000644�0000765�0000024�00000026513�11310767554�015651� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
The lamson.mail module contains nothing more than wrappers around the big work
done in lamson.encoding.  These are the actual APIs that you'll interact with
when doing email, and they mostly replicate the lamson.encoding.MailBase 
functionality.

The main design criteria is that MailRequest is mostly for reading email 
that you've received, so it doesn't have functions for attaching files and such.
MailResponse is used when you are going to write an email, so it has the
APIs for doing attachments and such.
"""


import mimetypes
from lamson import encoding, bounce
from email.utils import parseaddr
import os
import warnings


# You can change this to 'Delivered-To' on servers that support it like Postfix
ROUTABLE_TO_HEADER='to'

def _decode_header_randomness(addr):
    """
    This fixes the given address so that it is *always* a set() of 
    just email addresses suitable for routing.
    """
    if not addr:
        return set()
    elif isinstance(addr, list):
        return set(parseaddr(a.lower())[1] for a in addr)
    elif isinstance(addr, basestring):
        return set([parseaddr(addr.lower())[1]])
    else:
        raise encoding.EncodingError("Address must be a string or a list not: %r", type(addr))


class MailRequest(object):
    """
    This is what's handed to your handlers for you to process.  The information
    you get out of this is *ALWAYS* in Python unicode and should be usable 
    by any API.  Modifying this object will cause other handlers that deal
    with it to get your modifications, but in general you don't want to do
    more than maybe tag a few headers.
    """
    def __init__(self, Peer, From, To, Data):
        """
        Peer is the remote peer making the connection (sometimes the queue
        name).  From and To are what you think they are.  Data is the raw
        full email as received by the server.

        NOTE:  It does not handle multiple From headers, if that's even
        possible.  It will parse the From into a list and take the first
        one.
        """

        self.original = Data
        self.base = encoding.from_string(Data)
        self.Peer = Peer
        self.From = From or self.base['from']
        self.To = To or self.base[ROUTABLE_TO_HEADER]

        if 'from' not in self.base: 
            self.base['from'] = self.From
        if 'to' not in self.base:
            # do NOT use ROUTABLE_TO here
            self.base['to'] = self.To

        self.route_to = _decode_header_randomness(self.To)
        self.route_from = _decode_header_randomness(self.From)

        if self.route_from:
            self.route_from = self.route_from.pop()
        else:
            self.route_from = None

        self.bounce = None


    def all_parts(self):
        """Returns all multipart mime parts.  This could be an empty list."""
        return self.base.parts


    def body(self):
        """
        Always returns a body if there is one.  If the message
        is multipart then it returns the first part's body, if
        it's not then it just returns the body.  If returns
        None then this message has nothing for a body.
        """
        if self.base.parts:
            return self.base.parts[0].body
        else:
            return self.base.body


    def __contains__(self, key):
        return self.base.__contains__(key)

    def __getitem__(self, name):
        return self.base.__getitem__(name)

    def __setitem__(self, name, val):
        self.base.__setitem__(name, val)

    def __delitem__(self, name):
        del self.base[name]

    def __str__(self):
        """
        Converts this to a string usable for storage into a queue or 
        transmission.
        """
        return encoding.to_string(self.base)

    def __repr__(self):
        return "From: %r" % [self.Peer, self.From, self.To]

    def keys(self):
        return self.base.keys()

    def to_message(self):
        """
        Converts this to a Python email message you can use to
        interact with the python mail APIs.
        """
        return encoding.to_message(self.base)

    def walk(self):
        """Recursively walks all attached parts and their children."""
        for x in self.base.walk():
            yield x

    def is_bounce(self, threshold=0.3):
        """
        Determines whether the message is a bounce message based on 
        lamson.bounce.BounceAnalzyer given threshold.  0.3 is a good
        conservative base.
        """
        if not self.bounce:
            self.bounce = bounce.detect(self)

        if self.bounce.score > threshold:
            return True
        else:
            return False

    @property
    def msg(self):
        warnings.warn("The .msg attribute is deprecated, use .base instead.  This will be gone in Lamson 1.0",
                          category=DeprecationWarning, stacklevel=2)
        return self.base



class MailResponse(object):
    """
    You are given MailResponse objects from the lamson.view methods, and
    whenever you want to generate an email to send to someone.  It has
    the same basic functionality as MailRequest, but it is designed to
    be written to, rather than read from (although you can do both).

    You can easily set a Body or Html during creation or after by
    passing it as __init__ parameters, or by setting those attributes.

    You can initially set the From, To, and Subject, but they are headers so
    use the dict notation to change them:  msg['From'] = 'joe@test.com'.

    The message is not fully crafted until right when you convert it with
    MailResponse.to_message.  This lets you change it and work with it, then
    send it out when it's ready.
    """
    def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
        self.Body = Body
        self.Html = Html
        self.base = encoding.MailBase([('To', To), ('From', From), ('Subject', Subject)])
        self.multipart = self.Body and self.Html
        self.attachments = []

    def __contains__(self, key):
        return self.base.__contains__(key)

    def __getitem__(self, key):
        return self.base.__getitem__(key)

    def __setitem__(self, key, val):
        return self.base.__setitem__(key, val)

    def __delitem__(self, name):
        del self.base[name]

    def attach(self, filename=None, content_type=None, data=None, disposition=None):
        """
        Simplifies attaching files from disk or data as files.  To attach simple
        text simple give data and a content_type.  To attach a file, give the
        data/content_type/filename/disposition combination.

        For convenience, if you don't give data and only a filename, then it
        will read that file's contents when you call to_message() later.  If you 
        give data and filename then it will assume you've filled data with what
        the file's contents are and filename is just the name to use.
        """
        assert filename or data, "You must give a filename or some data to attach."
        assert data or os.path.exists(filename), "File doesn't exist, and no data given."

        self.multipart = True

        if filename and not content_type:
            content_type, encoding = mimetypes.guess_type(filename)

        assert content_type, "No content type given, and couldn't guess from the filename: %r" % filename

        self.attachments.append({'filename': filename,
                                 'content_type': content_type,
                                 'data': data,
                                 'disposition': disposition,})
    def attach_part(self, part):
        """
        Attaches a raw MailBase part from a MailRequest (or anywhere)
        so that you can copy it over.
        """
        self.multipart = True

        self.attachments.append({'filename': None,
                                 'content_type': None,
                                 'data': None,
                                 'disposition': None,
                                 'part': part,
                                 })

    def attach_all_parts(self, mail_request):
        """
        Used for copying the attachment parts of a mail.MailRequest
        object for mailing lists that need to maintain attachments.
        """
        for part in mail_request.all_parts():
            self.attach_part(part)

        self.base.content_encoding = mail_request.base.content_encoding.copy()

    def clear(self):
        """
        Clears out the attachments so you can redo them.  Use this to keep the
        headers for a series of different messages with different attachments.
        """
        del self.attachments[:]
        del self.base.parts[:]
        self.multipart = False


    def update(self, message):
        """
        Used to easily set a bunch of heading from another dict
        like object.
        """
        for k in message.keys():
            self.base[k] = message[k]

    def __str__(self):
        """
        Converts to a string.
        """
        return self.to_message().as_string()

    def _encode_attachment(self, filename=None, content_type=None, data=None, disposition=None, part=None):
        """
        Used internally to take the attachments mentioned in self.attachments
        and do the actual encoding in a lazy way when you call to_message.
        """
        if part:
            self.base.parts.append(part)
        elif filename:
            if not data:
                data = open(filename).read()

            self.base.attach_file(filename, data, content_type, disposition or 'attachment')
        else:
            self.base.attach_text(data, content_type)

        ctype = self.base.content_encoding['Content-Type'][0]

        if ctype and not ctype.startswith('multipart'):
            self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})

    def to_message(self):
        """
        Figures out all the required steps to finally craft the
        message you need and return it.  The resulting message
        is also available as a self.base attribute.

        What is returned is a Python email API message you can
        use with those APIs.  The self.base attribute is the raw
        lamson.encoding.MailBase.
        """
        del self.base.parts[:]

        if self.Body and self.Html:
            self.multipart = True
            self.base.content_encoding['Content-Type'] = ('multipart/alternative', {})

        if self.multipart:
            self.base.body = None
            if self.Body:
                self.base.attach_text(self.Body, 'text/plain')

            if self.Html:
                self.base.attach_text(self.Html, 'text/html')

            for args in self.attachments:
                self._encode_attachment(**args)

        elif self.Body:
            self.base.body = self.Body
            self.base.content_encoding['Content-Type'] = ('text/plain', {})

        elif self.Html:
            self.base.body = self.Html
            self.base.content_encoding['Content-Type'] = ('text/html', {})

        return encoding.to_message(self.base)

    def all_parts(self):
        """
        Returns all the encoded parts.  Only useful for debugging
        or inspecting after calling to_message().
        """
        return self.base.parts

    def keys(self):
        return self.base.keys()

    @property
    def msg(self):
        warnings.warn("The .msg attribute is deprecated, use .base instead.  This will be gone in Lamson 1.0",
                          category=DeprecationWarning, stacklevel=2)
        return self.base
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/queue.py���������������������������������������������������������������������0000644�0000765�0000024�00000013430�11310755342�016035� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Simpler queue management than the regular mailbox.Maildir stuff.  You
do get a lot more features from the Python library, so if you need
to do some serious surgery go use that.  This works as a good
API for the 90% case of "put mail in, get mail out" queues.
"""

import mailbox
from lamson import mail
import hashlib
import socket
import time
import os
import errno
import logging

# we calculate this once, since the hostname shouldn't change for every
# email we put in a queue
HASHED_HOSTNAME = hashlib.md5(socket.gethostname()).hexdigest()

class SafeMaildir(mailbox.Maildir):
    def _create_tmp(self):
        now = time.time()
        uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
                                    mailbox.Maildir._count, HASHED_HOSTNAME)
        path = os.path.join(self._path, 'tmp', uniq)
        try:
            os.stat(path)
        except OSError, e:
            if e.errno == errno.ENOENT:
                mailbox.Maildir._count += 1
                try:
                    return mailbox._create_carefully(path)
                except OSError, e:
                    if e.errno != errno.EEXIST:
                        raise
            else:
                raise

        # Fall through to here if stat succeeded or open raised EEXIST.
        raise mailbox.ExternalClashError('Name clash prevented file creation: %s' % path)


class QueueError(Exception):

    def __init__(self, msg, data):
        Exception.__init__(self, msg)
        self._message = msg
        self.data = data


class Queue(object):
    """
    Provides a simplified API for dealing with 'queues' in Lamson.
    It currently just supports maildir queues since those are the 
    most robust, but could implement others later.
    """

    def __init__(self, queue_dir, safe=False, pop_limit=0, oversize_dir=None):
        """
        This gives the Maildir queue directory to use, and whether you want
        this Queue to use the SafeMaildir variant which hashes the hostname
        so you can expose it publicly.

        The pop_limit and oversize_queue both set a upper limit on the mail
        you pop out of the queue.  The size is checked before any Lamson
        processing is done and is based on the size of the file on disk.  The
        purpose is to prevent people from sending 10MB attachments.  If a
        message is over the pop_limit then it is placed into the
        oversize_dir (which should be a maildir).

        The oversize protection only works on pop messages off, not
        putting them in, get, or any other call.  If you use get you can
        use self.oversize to also check if it's oversize manually.
        """
        self.dir = queue_dir

        if safe:
            self.mbox = SafeMaildir(queue_dir)
        else:
            self.mbox = mailbox.Maildir(queue_dir)

        self.pop_limit = pop_limit

        if oversize_dir:
            if not os.path.exists(oversize_dir):
                osmb = mailbox.Maildir(oversize_dir)

            self.oversize_dir = os.path.join(oversize_dir, "new")

            if not os.path.exists(self.oversize_dir):
                os.mkdir(self.oversize_dir)
        else:
            self.oversize_dir = None

    def push(self, message):
        """
        Pushes the message onto the queue.  Remember the order is probably
        not maintained.  It returns the key that gets created.
        """
        return self.mbox.add(str(message))

    def pop(self):
        """
        Pops a message off the queue, order is not really maintained
        like a stack.

        It returns a (key, message) tuple for that item.
        """
        for key in self.mbox.iterkeys():
            over, over_name =  self.oversize(key)

            if over:
                if self.oversize_dir:
                    logging.info("Message key %s over size limit %d, moving to %s.",
                                key, self.pop_limit, self.oversize_dir)
                    os.rename(over_name, os.path.join(self.oversize_dir, key))
                else:
                    logging.info("Message key %s over size limit %d, DELETING (set oversize_dir).", 
                                key, self.pop_limit)
                    os.unlink(over_name)
            else:
                try:
                    msg = self.get(key)
                except QueueError, exc:
                    raise exc
                finally:
                    self.remove(key)
                return key, msg

        return None, None

    def get(self, key):
        """
        Get the specific message referenced by the key.  The message is NOT
        removed from the queue.
        """
        msg_file = self.mbox.get_file(key)

        if not msg_file: 
            return None

        msg_data = msg_file.read()

        try:
            return mail.MailRequest(self.dir, None, None, msg_data)
        except Exception, exc:
            raise QueueError("Failed to decode message: %s" % exc, msg_data)


    def remove(self, key):
        """Removes the queue, but not returned."""
        self.mbox.remove(key)
    
    def count(self):
        """Returns the number of messages in the queue."""
        return len(self.mbox)

    def clear(self):
        """
        Clears out the contents of the entire queue.
        Warning: This could be horribly inefficient since it
        basically pops until the queue is empty.
        """
        # man this is probably a really bad idea
        while self.count() > 0:
            self.pop()
    
    def keys(self):
        """
        Returns the keys in the queue.
        """
        return self.mbox.keys()

    def oversize(self, key):
        if self.pop_limit:
            file_name = os.path.join(self.dir, "new", key)
            return os.path.getsize(file_name) > self.pop_limit, file_name
        else:
            return False, None



����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/routing.py�������������������������������������������������������������������0000644�0000765�0000024�00000055544�11311243577�016417� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
"""
The meat of Lamson, doing all the work that actually takes an email and makes
sure that your code gets it.

The three most important parts for a programmer are the Router variable, the
StateStorage base class, and the @route, @route_like, and @stateless decorators.

The lamson.routing.Router variable (it's not a class, just named like one) is
how the whole system gets to the Router.  It is an instance of RoutingBase and
there's usually only one.

The lamson.routing.StateStorage is what you need to implement if you want Lamson
to store the state in a different way.  By default the lamson.routing.Router
object just uses a default MemoryStorage to do its job.  If you want to use a
custom storage, then in your config/boot.py (or config/testing.py) you would set
lamson.routing.Router.STATE_STORE to what you want to use.

Finally, when you write a state handler, it has functions that act as state
functions for dealing with each state.  To tell the Router what function should
handle what email you use a @route decorator.  To tell the Route that one
function routes the same as another use @route_like.  In the case where a state
function should run on every matching email, just use the @stateless decorator
after a @route or @route_like.

If at any time you need to debug your routing setup just use the lamson routes
command.

Routing Control
===============

To control routing there are a set of decorators that you apply to your
functions.

* @route -- The main routing function that determines what addresses you are
interested in.
* @route_like -- Says that this function routes like another one.
* @stateless -- Indicates this function always runs on each route encountered, and
no state is maintained.
* @nolocking -- Use this if you want this handler to run parallel without any
locking around Lamson internals.  SUPER DANGEROUS, add @stateless as well.
* @state_key_generator -- Used on a function that knows how to make your state
keys for the module, for example if module_name + message.route_to is needed to maintain
state.

It's best to put @route or @route_like as the first decorator, then the others 
after that.

The @state_key_generator is different since it's not intended to go on a handler
but instead on a simple function, so it shouldn't be combined with the others.
"""

from __future__ import with_statement
from functools import wraps
import re
import logging
import sys
import email.utils
import shelve
import threading

ROUTE_FIRST_STATE = 'START'
LOG = logging.getLogger("routing")
DEFAULT_STATE_KEY = lambda mod, msg: mod


class StateStorage(object):
    """
    The base storage class you need to implement for a custom storage
    system.
    """
    def get(self, key, sender):
        """
        You must implement this so that it returns a single string
        of either the state for this combination of arguments, OR
        the ROUTE_FIRST_STATE setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.get.")

    def set(self, key, sender, state):
        """
        Set should take the given parameters and consistently set the state for 
        that combination such that when StateStorage.get is called it gives back
        the same setting.
        """
        raise NotImplementedError("You have to implement a StateStorage.set.")

    def clear(self):
        """
        This should clear ALL states, it is only used in unit testing, so you 
        can have it raise an exception if you want to make this safer.
        """
        raise NotImplementedError("You have to implement a StateStorage.clear for unit testing to work.")


class MemoryStorage(StateStorage):
    """
    The default simplified storage for the Router to hold the states.  This
    should only be used in testing, as you'll lose all your contacts and their
    states if your server shutsdown.  It is also horribly NOT thread safe.
    """
    def __init__(self):
        self.states = {}

    def get(self, key, sender):
        key = self.key(key, sender)
        try:
            return self.states[key]
        except KeyError:
            return ROUTE_FIRST_STATE

    def set(self, key, sender, state):
        key = self.key(key, sender)
        if state == ROUTE_FIRST_STATE:
            try:
                del self.states[key]
            except KeyError:
                pass
        else:
            self.states[key] = state

    def key(self, key, sender):
        return repr([key, sender])

    def clear(self):
        self.states.clear()


class ShelveStorage(MemoryStorage):
    """
    Uses Python's shelve to store the state of the Routers to disk rather than
    in memory like with MemoryStorage.  This will get you going on a small
    install if you need to persist your states (most likely), but if you 
    have a database, you'll need to write your own StateStorage that 
    uses your ORM or database to store.  Consider this an example.

    NOTE: Because of shelve limitations you can only use ASCII encoded keys.
    """
    def __init__(self, database_path):
        """Database path depends on the backing library use by Python's shelve."""
        self.database_path = database_path
        self.lock = threading.RLock()

    def get(self, key, sender):
        """
        This will lock the internal thread lock, and then retrieve from the
        shelf whatever key you request.  If the key is not found then it
        will set (atomically) to ROUTE_FIRST_STATE.
        """
        with self.lock:
            self.states = shelve.open(self.database_path)
            value = super(ShelveStorage, self).get(key.encode('ascii'), sender)
            self.states.close()
            return value

    def set(self, key, sender, state):
        """
        Acquires the self.lock and then sets the requested state in the shelf.
        """
        with self.lock:
            self.states = shelve.open(self.database_path)
            super(ShelveStorage, self).set(key.encode('ascii'), sender, state)
            self.states.close()

    def clear(self):
        """
        Primarily used in the debugging/unit testing process to make sure the
        states are clear.  In production this could be a bad thing.
        """
        with self.lock:
            self.states = shelve.open(self.database_path)
            super(ShelveStorage, self).clear()
            self.states.close()



class RoutingBase(object):
    """
    The self is a globally accessible class that is actually more like a
    glorified module.  It is used mostly internally by the lamson.routing 
    decorators (route, route_like, stateless) to control the routing 
    mechanism.

    It keeps track of the registered routes, their attached functions, the
    order that these routes should be evaluated, any default routing captures,
    and uses the MemoryStorage by default to keep track of the states.

    You can change the storage to another implementation by simple setting:

        self.STATE_STORE = OtherStorage()

    In a config/settings.py file.

    RoutingBase does locking on every write to its internal data (which usually
    only happens during booting and reloading while debugging), and when each
    handler's state function is called.  ALL threads will go through this lock,
    but only as each state is run, so you won't have a situation where the chain
    of state functions will block all the others.  This means that while your
    handler runs nothing will be running, but you have not guarantees about 
    the order of each state function.

    However, this can kill the performance of some kinds of state functions,
    so if you find the need to not have locking, then use the @nolocking 
    decorator and the Router will NOT lock when that function is called.  That
    means while your @nolocking state function is running at least one other
    thread (more if the next ones happen to be @nolocking) could also be
    running.

    It's your job to keep things straight if you do that.

    NOTE: See @state_key_generator for a way to change what the key is to 
    STATE_STORE for different state control options.
    """

    def __init__(self):
        self.REGISTERED = {}
        self.ORDER = []
        self.DEFAULT_CAPTURES = {}
        self.STATE_STORE = MemoryStorage()
        self.HANDLERS = {}
        self.RELOAD = False
        self.LOG_EXCEPTIONS = True
        self.UNDELIVERABLE_QUEUE = None
        self.lock = threading.RLock()
        self.call_lock = threading.RLock()

    def register_route(self, format, func):
        """
        Registers this function func into the routes mapping based on the
        format given.  Format should be a regex string ready to be handed to
        re.compile.
        """
        with self.lock:
            if format in self.REGISTERED:
                self.REGISTERED[format][1].append(func)
            else:
                self.ORDER.append(format)
                self.REGISTERED[format] = (re.compile(format, re.IGNORECASE), [func])

    def match(self, address):
        """
        This is a generator that goes through all the routes and
        yields each match it finds.  It expects you to give it a
        blah@blah.com address, NOT "Joe Blow" <blah@blah.com>.
        """
        for format in self.ORDER:
            regex, functions = self.REGISTERED[format]
            match = regex.match(address)
            if match:
                yield functions, match.groupdict()

    def defaults(self, **captures):
        """
        Updates the defaults for routing captures with the given settings.

        You use this in your handlers or your config/settings.py to set
        common regular expressions you'll have in your @route decorators.
        This saves you typing, but also makes it easy to reconfigure later.

        For example, many times you'll have a single host="..." regex
        for all your application's routes.  Put this in your settings.py
        file using route_defaults={'host': '...'} and you're done.
        """
        with self.lock:
            self.DEFAULT_CAPTURES.update(captures)

    def get_state(self, module_name, message):
        """Returns the state that this module is in for the given message (using its from)."""
        key = self.state_key(module_name, message)
        return self.STATE_STORE.get(key, message.route_from)

    
    def in_state(self, func, message):
        """
        Determines if this function is in the state for the to/from in the
        message.  Doesn't apply to @stateless state handlers.
        """
        state = self.get_state(func.__module__, message)
        return state and state == func.__name__

    def in_error(self, func, message):
        """
        Determines if the this function is in the 'ERROR' state, 
        which is a special state that self puts handlers in that throw
        an exception.
        """
        state = self.get_state(func.__module__, message)
        return state and state == 'ERROR'

    def state_key(self, module_name, message):
        """
        Given a module_name we need to get a state key for, and a
        message that has information to make the key, this function
        calls any registered @state_key_generator and returns that
        as the key.  If none is given then it just returns module_name
        as the key.
        """
        key_func = self.HANDLERS.get(module_name, DEFAULT_STATE_KEY)
        return key_func(module_name, message)

    def set_state(self, module_name, message, state):
        """
        Sets the state of the given module (a string) according to the message to the requested
        state (a string).  This is also how you can force another FSM to a required state.
        """
        key = self.state_key(module_name, message)
        self.STATE_STORE.set(key, message.route_from, state)

    def _collect_matches(self, message, route_to):
        in_state_found = False

        for functions, matchkw in self.match(route_to):
            for func in functions:
                if lamson_setting(func, 'stateless'):
                    yield func, matchkw
                elif not in_state_found and self.in_state(func, message):
                    in_state_found = True
                    yield func, matchkw

    def _enqueue_undeliverable(self, message):
        if self.UNDELIVERABLE_QUEUE:
            LOG.debug("Message to %r from %r undeliverable, putting in undeliverable queue (# of recipients: %d).",
                      message.route_to, message.route_from, len(message.route_to))
            self.UNDELIVERABLE_QUEUE.push(message)
        else:
            LOG.debug("Message to %r from %r didn't match any handlers. (# recipients: %d)",
                      message.route_to, message.route_from, len(message.route_to))

    def deliver(self, message):
        """
        The meat of the whole Lamson operation, this method takes all the
        arguments given, and then goes through the routing listing to figure out
        which state handlers should get the gear.  The routing operates on a
        simple set of rules:

            1) Match on all functions that match the given To in their
            registered format pattern.
            2) Call all @stateless state handlers functions.
            3) Call the first method that's in the right state for the From/To.

        It will log which handlers are being run, and you can use the 'lamson route'
        command to inspect and debug routing problems.

        If you have an ERROR state function, then when your state blows up, it will
        transition to ERROR state and call your function right away.  It will then
        stay in the ERROR state unless you return a different one.
        """
        if self.RELOAD: self.reload()

        called_count = 0

        for routing_on in message.route_to:
            for func, matchkw in self._collect_matches(message, routing_on):
                LOG.debug("Matched %r against %s.", routing_on, func.__name__)

                if lamson_setting(func, 'nolocking'):
                    self.call_safely(func, message,  matchkw)
                else:
                    with self.call_lock:
                        self.call_safely(func, message, matchkw)

                called_count += 1

        if called_count == 0:
            self._enqueue_undeliverable(message)


    def call_safely(self, func, message, kwargs):
        """
        Used by self to call a function and log exceptions rather than
        explode and crash.
        """
        from lamson.server import SMTPError

        try:
            func(message, **kwargs)
            LOG.debug("Message to %s was handled by %s.%s",
                          message.route_to, func.__module__, func.__name__)
        except SMTPError:
            raise
        except:
            self.set_state(func.__module__, message, 'ERROR')

            if self.UNDELIVERABLE_QUEUE:
                self.UNDELIVERABLE_QUEUE.push(message)

            if self.LOG_EXCEPTIONS:
                LOG.exception("!!! ERROR handling %s.%s", func.__module__, func.__name__)
            else:
                raise


    def clear_states(self):
        """Clears out the states for unit testing."""
        with self.lock:
            self.STATE_STORE.clear()

    def clear_routes(self):
        """Clears out the routes for unit testing and reloading."""
        with self.lock:
            self.REGISTERED.clear()
            del self.ORDER[:]

    
    def load(self, handlers):
        """
        Loads the listed handlers making them available for processing.
        This is safe to call multiple times and to duplicate handlers
        listed.
        """
        with self.lock:
            for module in handlers:
                try:
                    __import__(module, globals(), locals())

                    if module not in self.HANDLERS:
                        # they didn't specify a key generator, so use the
                        # default one for now
                        self.HANDLERS[module] = DEFAULT_STATE_KEY
                except:
                    if self.LOG_EXCEPTIONS:
                        LOG.exception("ERROR IMPORTING %r MODULE:" % module)
                    else:
                        raise

    def reload(self):
        """
        Performs a reload of all the handlers and clears out all routes,
        but doesn't touch the internal state.
        """
        with self.lock:
            self.clear_routes()
            for module in sys.modules.keys():
                if module in self.HANDLERS:
                    try:
                        reload(sys.modules[module])
                    except:
                        if self.LOG_EXCEPTIONS:
                            LOG.exception("ERROR RELOADING %r MODULE:" % module)
                        else:
                            raise

Router = RoutingBase()

class route(object):
    """
    The @route decorator is attached to state handlers to configure them in the
    Router so they handle messages for them.  The way this works is, rather than
    just routing working on only messages being sent to a state handler, it also uses
    the state of the sender.  It's like having routing in a web application use
    both the URL and an internal state setting to determine which method to run.

    However, if you'd rather than this state handler process all messages
    matching the @route then tag it @stateless.  This will run the handler 
    no matter what and not change the user's state.
    """

    def __init__(self, format, **captures):
        """
        Sets up the pattern used for the Router configuration.  The format
        parameter is a simple pattern of words, captures, and anything you
        want to ignore.  The captures parameter is a mapping of the words in
        the format to regex that get put into the format.  When the pattern is
        matched, the captures are handed to your state handler as keyword
        arguments.

        For example, if you have:

            @route("(list_name)-(action)@(host)",
                list_name='[a-z]+',
                action='[a-z]+', host='test\.com')
            def STATE(message, list_name=None, action=None, host=None):
                ....

        Then this will be translated so that list_name is replaced with [a-z]+,
        action with [a-z]+, and host with 'test.com' to produce a regex with the
        right format and named captures to that your state handler is called
        with the proper keyword parameters.

        You should also use the Router.defaults() to set default things like the
        host so that you are not putting it into your code.
        """
        self.captures = Router.DEFAULT_CAPTURES.copy()
        self.captures.update(captures)
        self.format = self.parse_format(format, self.captures)

    def __call__(self, func):
        """Returns either a decorator that does a stateless routing or
        a normal routing."""
        self.setup_accounting(func)

        if lamson_setting(func, 'stateless'):
            @wraps(func)
            def routing_wrapper(message, *args, **kw):
                next_state = func(message, *args, **kw)
        else:
            @wraps(func)
            def routing_wrapper(message, *args, **kw):
                next_state = func(message, *args, **kw)

                if next_state:
                    Router.set_state(next_state.__module__, message, next_state.__name__)

        Router.register_route(self.format, routing_wrapper)
        return routing_wrapper

    def __get__(self, obj, of_type=None):
        """
        This is NOT SUPPORTED.  It is here just so that if you try to apply
        this decorator to a class's method it will barf on you.
        """
        raise NotImplementedError("Not supported on methods yet, only module functions.")

    def parse_format(self, format, captures):
        """Does the grunt work of convertion format+captures into the regex."""
        for key in captures:
            format = format.replace("(" + key + ")", "(?P<%s>%s)" % (key, captures[key]))
        return "^" + format + "$"

    def setup_accounting(self, func):
        """Sets up an accounting map attached to the func for routing decorators."""
        attach_lamson_settings(func)
        func._lamson_settings['format'] = self.format
        func._lamson_settings['captures'] = self.captures


def lamson_setting(func, key):
    """Simple way to get the lamson setting off the function, or None."""
    return func._lamson_settings.get(key)


def has_lamson_settings(func):
    return "_lamson_settings" in func.__dict__

def assert_lamson_settings(func):
    """Used to make sure that the func has been setup by a routing decorator."""
    assert has_lamson_settings(func), "Function %s has not be setup with a @route first." % func.__name__


def attach_lamson_settings(func):
    """Use this to setup the _lamson_settings if they aren't already there."""
    if '_lamson_settings' not in func.__dict__:
        func._lamson_settings = {}


class route_like(route):
    """
    Many times you want your state handler to just accept mail like another
    handler.  Use this, passing in the other function.  It even works across
    modules.
    """
    def __init__(self, func):
        assert_lamson_settings(func)
        self.format = func._lamson_settings['format']
        self.captures = func._lamson_settings['captures']


def stateless(func):
    """
    This simple decorator is attached to a handler to indicate to the
    Router.deliver() method that it does NOT maintain state or care about it.
    This is how you create a handler that processes all messages matching the
    given format+captures in a @route.

    Another way to think about a @stateless handler is that it is a passthrough
    handler that does its processing and then passes the results on to others.

    Stateless handlers are NOT guaranteed to run before the handler with state.
    """
    if has_lamson_settings(func):
        assert not lamson_setting(func, 'format'), "You must use @stateless AFTER @route or @route_like."
    
    attach_lamson_settings(func)
    func._lamson_settings['stateless'] = True

    return func

def nolocking(func):
    """
    Normally lamson.routing.Router has a lock around each call to all handlers
    to prevent them from stepping on eachother.  It's assumed that 95% of the
    time this is what you want, so it's the default.  You probably want
    everything to go in order and not step on other things going off from other
    threads in the system.

    However, sometimes you know better what you are doing and this is where
    @nolocking comes in.  Put this decorator on your state functions that you
    don't care about threading issues or that you have found a need to 
    manually tune, and it will run it without any locks.
    """
    attach_lamson_settings(func)
    func._lamson_settings['nolocking'] = True
    return func

def state_key_generator(func):
    """
    Used to indicate that a function in your handlers should be used
    to determine what they key is for state storage.  It should be a 
    function that takes the module_name and message being worked on
    and returns a string.
    """
    Router.HANDLERS[func.__module__] = func
    return func
������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/server.py��������������������������������������������������������������������0000644�0000765�0000024�00000023562�11313444405�016224� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
The majority of the server related things Lamson needs to run, like receivers, 
relays, and queue processors.
"""

import smtplib
import smtpd
import asyncore
import threading
import socket
import logging
from lamson import queue, mail, routing
import time
import traceback
from lamson.bounce import PRIMARY_STATUS_CODES, SECONDARY_STATUS_CODES, COMBINED_STATUS_CODES


def undeliverable_message(raw_message, failure_type):
    """
    Used universally in this file to shove totally screwed messages
    into the routing.Router.UNDELIVERABLE_QUEUE (if it's set).
    """
    if routing.Router.UNDELIVERABLE_QUEUE:
        key = routing.Router.UNDELIVERABLE_QUEUE.push(raw_message)

        logging.error("Failed to deliver message because of %r, put it in "
                      "undeliverable queue with key %r", failure_type, key)

class SMTPError(Exception):
    """
    You can raise this error when you want to abort with a SMTP error code to
    the client.  This is really only relevant when you're using the
    SMTPReceiver and the client understands the error.

    If you give a message than it'll use that, but it'll also produce a
    consistent error message based on your code.  It uses the errors in
    lamson.bounce to produce them.
    """
    def __init__(self, code, message=None):
        self.code = code
        self.message = message or self.error_for_code(code)

        Exception.__init__(self, "%d %s" % (self.code, self.message))

    def error_for_code(self, code):
        primary, secondary, tertiary = str(code)
        
        primary = PRIMARY_STATUS_CODES.get(primary, "")
        secondary = SECONDARY_STATUS_CODES.get(secondary, "")
        combined = COMBINED_STATUS_CODES.get(primary + secondary, "")

        return " ".join([primary, secondary, combined]).strip()


class Relay(object):
    """
    Used to talk to your "relay server" or smart host, this is probably the most 
    important class in the handlers next to the lamson.routing.Router.
    It supports a few simple operations for sending mail, replying, and can
    log the protocol it uses to stderr if you set debug=1 on __init__.
    """
    def __init__(self, host='127.0.0.1', port=25, username=None, password=None,
                 ssl=False, starttls=False, debug=0):
        """
        The hostname and port we're connecting to, and the debug level (default to 0).
        Optional username and password for smtp authentication.
        If ssl is True smtplib.SMTP_SSL will be used.
        If starttls is True (and ssl False), smtp connection will be put in TLS mode.
        It does the hard work of delivering messages to the relay host.
        """
        self.hostname = host
        self.port = port
        self.debug = debug
        self.username = username
        self.password = password
        self.ssl = ssl
        self.starttls = starttls

    def configure_relay(self, hostname):
        if self.ssl:
            relay_host = smtplib.SMTP_SSL(hostname, self.port)
        else:
            relay_host = smtplib.SMTP(hostname, self.port)

        relay_host.set_debuglevel(self.debug)

        if self.starttls:
            relay_host.starttls()
        if self.username and self.password:
            relay_host.login(self.username, self.password)

        assert relay_host, 'Code error, tell Zed.'
        return relay_host

    def deliver(self, message, To=None, From=None):
        """
        Takes a fully formed email message and delivers it to the
        configured relay server.

        You can pass in an alternate To and From, which will be used in the
        SMTP send lines rather than what's in the message.
        """
        recipient = To or message['To']
        sender = From or message['From']

        hostname = self.hostname or self.resolve_relay_host(recipient)

        try:
            relay_host = self.configure_relay(hostname)
        except socket.error:
            logging.exception("Failed to connect to host %s:%d" % (hostname, self.port))
            return

        relay_host.sendmail(sender, recipient, str(message))
        relay_host.quit()

    def resolve_relay_host(self, To):
        import DNS
        address, target_host = To.split('@')
        mx_hosts = DNS.mxlookup(target_host)

        if not mx_hosts:
            logging.debug("Domain %r does not have an MX record, using %r instead.", target_host, target_host)
            return target_host
        else:
            logging.debug("Delivering to MX record %r for target %r", mx_hosts[0], target_host)
            return mx_hosts[0][1]


    def __repr__(self):
        """Used in logging and debugging to indicate where this relay goes."""
        return "<Relay to (%s:%d)>" % (self.hostname, self.port)


    def reply(self, original, From, Subject, Body):
        """Calls self.send but with the from and to of the original message reversed."""
        self.send(original['from'], From=From, Subject=Subject, Body=Body)

    def send(self, To, From, Subject, Body):
        """
        Does what it says, sends an email.  If you need something more complex
        then look at lamson.mail.MailResponse.
        """
        msg = mail.MailResponse(To=To, From=From, Subject=Subject, Body=Body)
        self.deliver(msg)



class SMTPReceiver(smtpd.SMTPServer):
    """Receives emails and hands it to the Router for further processing."""

    def __init__(self, host='127.0.0.1', port=8825):
        """
        Initializes to bind on the given port and host/ipaddress.  Typically
        in deployment you'd give 0.0.0.0 for "all internet devices" but consult
        your operating system.

        This uses smtpd.SMTPServer in the __init__, which means that you have to 
        call this far after you use python-daemonize or else daemonize will
        close the socket.
        """
        self.host = host
        self.port = port
        smtpd.SMTPServer.__init__(self, (self.host, self.port), None)

    def start(self):
        """
        Kicks everything into gear and starts listening on the port.  This
        fires off threads and waits until they are done.
        """
        logging.info("SMTPReceiver started on %s:%d." % (self.host, self.port))
        self.poller = threading.Thread(target=asyncore.loop,
                kwargs={'timeout':0.1, 'use_poll':True})
        self.poller.start()

    def process_message(self, Peer, From, To, Data):
        """
        Called by smtpd.SMTPServer when there's a message received.
        """

        try:
            logging.debug("Message received from Peer: %r, From: %r, to To %r." % (Peer, From, To))
            routing.Router.deliver(mail.MailRequest(Peer, From, To, Data))
        except SMTPError, err:
            # looks like they want to return an error, so send it out
            return str(err)
            undeliverable_message(Data, "Handler raised SMTPError on purpose: %s" % err)
        except:
            logging.exception("Exception while processing message from Peer: %r, From: %r, to To %r." %
                          (Peer, From, To))
            undeliverable_message(Data, "Error in message %r:%r:%r, look in logs." % (Peer, From, To))


    def close(self):
        """Doesn't do anything except log who called this, since nobody should.  Ever."""
        logging.error(traceback.format_exc())


class QueueReceiver(object):
    """
    Rather than listen on a socket this will watch a queue directory and
    process messages it recieves from that.  It works in almost the exact
    same way otherwise.
    """

    def __init__(self, queue_dir, sleep=10, size_limit=0, oversize_dir=None):
        """
        The router should be fully configured and ready to work, the
        queue_dir can be a fully qualified path or relative.
        """
        self.queue = queue.Queue(queue_dir, pop_limit=size_limit,
                                 oversize_dir=oversize_dir)
        self.queue_dir = queue_dir
        self.sleep = sleep

    def start(self, one_shot=False):
        """
        Start simply loops indefinitely sleeping and pulling messages
        off for processing when they are available.

        If you give one_shot=True it will run once rather than do a big
        while loop with a sleep.
        """

        logging.info("Queue receiver started on queue dir %s" %
                     (self.queue_dir))
        logging.debug("Sleeping for %d seconds..." % self.sleep)

        inq = queue.Queue(self.queue_dir)

        while True:
            key = None

            try:
                key, msg = inq.pop()

                while key:
                    logging.debug("Pulled message with key: %r off", key)
                    self.process_message(msg)
                    key, msg = inq.pop()

                if one_shot: 
                    return
                else:
                    time.sleep(self.sleep)

            except:
                logging.exception("Error popping from the queue, this might be a problem.")
                undeliverable_message(exc.data, exc._message)
                time.sleep(self.sleep)

    def process_message(self, msg):
        """
        Exactly the same as SMTPReceiver.process_message but just designed for the queue's
        quirks.
        """

        try:
            Peer = self.queue_dir # this is probably harmless but I should check it
            From = msg['from']
            To = [msg['to']]

            logging.debug("Message received from Peer: %r, From: %r, to To %r." % (Peer, From, To))
            routing.Router.deliver(msg)
        except SMTPError, err:
            # looks like they want to return an error, so send it out
            logging.exception("Raising SMTPError when running in a QueueReceiver is unsupported.")
            undeliverable_message(msg.original, err.message)
        except:
            logging.exception("Exception while processing message from Peer: "
                              "%r, From: %r, to To %r." % (Peer, From, To))
            undeliverable_message(msg.original, "Router failed to catch exception.")





����������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/spam.py����������������������������������������������������������������������0000644�0000765�0000024�00000011244�11242111636�015646� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Uses the SpamBayes system to perform filtering and classification
of email.  It's designed so that you attach a single decorator
to the state functions you need to be "spam free", and then use the
lamson.spam.Filter code to do training.

SpamBayes comes with extensive command line tools for processing
maildir and mbox for spam.  A good way to train SpamBayes is to 
take mail that you know is spam and stuff it into a maildir, then
periodically use the SpamBayes tools to train from that.
"""

from functools import wraps
from lamson import queue
from spambayes import hammie, Options, storage
import os
import logging

class Filter(object):
    """
    This code implements simple filtering and is taken from the
    SpamBayes documentation.
    """
    def __init__(self, storage_file, config):
        options = Options.options
        options["Storage", "persistent_storage_file"] = storage_file
        options.merge_files(['/etc/hammierc', os.path.expanduser(config)])

        self.include_trained = Options.options["Headers", "include_trained"]
        self.dbname, self.usedb = storage.database_type([])

        self.mode = None
        self.h = None

        assert not Options.options["Hammie", "train_on_filter"], "Cannot train_on_filter."

    def open(self, mode):
        assert not self.h, "Cannot reopen, close first."
        assert not self.mode, "Mode should be None on open, bad state."
        assert mode in ['r', 'c'], "Must give a valid mode: r, c."

        self.mode = mode
        self.h = hammie.open(self.dbname, self.usedb, self.mode)

    def close(self):
        if not self.h: return

        assert self.mode, "Mode was not set."
        assert self.mode in ['r','c'], "self.mode was not r or c. Bad state."

        if self.mode == 'c':
            self.h.store()
            self.h.close()

        self.h = None
        self.mode = None


    def filter(self, msg):
        self.open('r')
        result = self.h.filter(msg)
        self.close()
        return result

    def train_ham(self, msg):
        self.open('c')
        self.h.train_ham(msg, self.include_trained)
        self.close()

    def train_spam(self, msg):
        self.open('c')
        self.h.train_spam(msg, self.include_trained)
        self.close()

    def untrain_ham(self, msg):
        self.open('c')
        self.h.untrain_ham(msg)
        self.close()

    def untrain_spam(self, msg):
        self.open('c')
        self.h.untrain_spam(msg)
        self.close()




class spam_filter(object):
    """
    This is a decorator you attach to states that should be protected from spam.
    You use it by doing:

        @spam_filter(ham_db, rcfile, spam_dump_queue, next_state=SPAMMING)

    Where ham_db is the path to your hamdb configuration, rcfile is the 
    SpamBayes config, and spam_dump_queue is where this filter should
    dump spam it detects.

    The next_state argument is optional, defaulting to None, but if you use
    it then Lamson will transition that user into that state.  Use it to mark
    that address as a spammer and to ignore their emails or do something
    fancy with them.
    """

    def __init__(self, storage, config, spam_queue, next_state=None):
        self.storage = storage
        self.config = config
        self.spam_queue = spam_queue
        self.next_state = next_state
        assert self.next_state, "You must give next_state function."

        if not os.path.exists(self.storage):
            logging.warn("SPAM filter for %r does not have a valid storage path, it'll still run but won't do anything.",
                        (self.storage, self.config, self.spam_queue,
                         self.next_state.__name__))
            self.functioning = False
        else:
            self.functioning = True

    def __call__(self, fn):
        @wraps(fn)
        def category_wrapper(message, *args, **kw):
            if self.functioning:
                if self.spam(message.to_message()):
                    self.enqueue_as_spam(message.to_message())
                    return self.next_state
                else:
                    return fn(message, *args, **kw)
            else:
                return fn(message, *args, **kw)
        return category_wrapper

    def spam(self, message):
        """Determines if the message is spam or not."""
        spfilter = Filter(self.storage, self.config)
        spfilter.filter(message)

        if 'X-Spambayes-Classification' in message:
            return message['X-Spambayes-Classification'].startswith('spam')
        else:
            return False

    def enqueue_as_spam(self, message):
        """Drops the message into the configured spam queue."""
        outq = queue.Queue(self.spam_queue)
        outq.push(str(message))

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/testing.py�������������������������������������������������������������������0000644�0000765�0000024�00000014075�11227420357�016376� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
A bag of generally useful things when writing unit tests for your Lamson server.
The most important things are the spelling function and using the
TestConversation vs. RouterConversation to talk to your server.

The TestConversation will use the lamson.server.Relay you have configured to
talk to your actual running Lamson server.  Since by default Lamson reloads each
file you change it will work to run your tests.

However, this isn't that fast, doesn't give you coverage analysis, and doesn't
let you test the results.  For that you use RouterConversation to do the exact
same API (they should be interchangeable) but rather than talk to a running
server through the relay, it just runs all the messages through the router
directly. 

This is faster and will give you code coverage as well as make sure that all the
modules (not just your handlers) will get reloaded.

The spelling function will use PyEnchant to spell check a string.  If it finds
any errors it prints them out, and returns False.
"""


from lamson import server, utils, routing, mail
from lamson.queue import Queue
from nose.tools import assert_equal
import re
import logging

TEST_QUEUE = "run/queue"


def spelling(file_name, contents, language="en_US"):
    """
    You give it a file_name and the contents of that file and it tells you
    if it's spelled correctly.  The reason you give it contents is that you
    will typically run a template through the render process, so spelling 
    can't just load a file and check it.

    It assumes you have PyEnchant installed correctly and configured 
    in your config/testing.py file.  Use "lamson spell" to make sure it
    works right.
    """
    try:
        from enchant.checker import SpellChecker 
        from enchant.tokenize import EmailFilter, URLFilter 
    except:
        print "Failed to load PyEnchant.  Make sure it's installed and lamson spell works."
        return True

    failures = 0
    chkr = SpellChecker(language, filters=[EmailFilter, URLFilter]) 
    chkr.set_text(contents)
    for err in chkr:
        print "%s: %s \t %r" % (file_name, err.word, contents[err.wordpos-20:err.wordpos+20])
        failures += 1

    if failures:
        print "You have %d spelling errors in %s.  Run lamson spell.." % (failures, file_name)
        return False
    else:
        return True




def relay(hostname="127.0.0.1", port=8824):
    """Wires up a default relay on port 8824 (the default lamson log port)."""
    return server.Relay(hostname, port, debug=0)


def queue(queue_dir=TEST_QUEUE):
    """Creates a queue for you to analyze the results of a send, uses the
    TEST_QUEUE setting in settings.py if that exists, otherwise defaults to
    run/queue."""
    return Queue(queue_dir)


def clear_queue(queue_dir=TEST_QUEUE):
    """Clears the default test queue out, as created by lamson.testing.queue."""
    queue(queue_dir).clear()


def delivered(pattern, to_queue=None):
    """
    Checks that a message with that patter is delivered, and then returns it.

    It does this by searching through the queue directory and finding anything that
    matches the pattern regex.
    """
    inq = to_queue or queue()
    for key in inq.keys():
        msg = inq.get(key)
        if not msg:
            # no messages in the queue
            return False

        regp = re.compile(pattern)
        if regp.search(str(msg)):
            msg = inq.get(key)
            return msg

    # didn't find anything
    return False


class TestConversation(object):
    """
    Used to easily do conversations with an email server such that you
    send a message and then expect certain responses.
    """

    def __init__(self, relay_to_use, From, Subject):
        """
        This creates a set of default values for the conversation so that you
        can easily send most basic message.  Each method lets you override the
        Subject and Body when you send.
        """
        self.relay = relay_to_use
        self.From = From
        self.Subject = Subject

    def begin(self):
        """Clears out the queue and Router states so that you have a fresh start."""
        clear_queue()
        routing.Router.clear_states()

    def deliver(self, To, From, Subject, Body):
        """Delivers it to the relay."""
        self.relay.send(To, From, Subject, Body)

    def say(self, To, Body, expect=None, Subject=None):
        """
        Say something to To and expect a reply with a certain address.
        It returns the message expected or None.
        """
        msg = None

        self.deliver(To, self.From, Subject or self.Subject, Body)
        if expect:
            msg = delivered(expect)
            if not msg:
                print "MESSAGE IN QUEUE:"
                inq = queue()
                for key in inq.keys():
                    print "-----"
                    print inq.get(key)

            assert msg, "Expected %r when sending to %r with '%s:%s' message." % (expect, 
                                          To, self.Subject or Subject, Body)
        return msg

class RouterConversation(TestConversation):
    """
    An implementation of TestConversation that routes the messages
    internally to the Router, rather than connecting with a relay.
    Use it in tests that are not integration tests.
    """
    def __init__(self, From, Subject):
        self.From = From
        self.Subject = Subject

    def deliver(self, To, From, Subject, Body):
        """Overrides TestConversation.deliver to do it internally."""
        sample = mail.MailResponse(From=From, To=To, Subject=Subject, Body=Body)
        msg = mail.MailRequest('localhost', sample['From'], sample['To'], str(sample))
        routing.Router.deliver(msg)



def assert_in_state(module, To, From, state):
    """
    Makes sure a user is in a certain state for a certain user.
    Use these sparingly, since every time you change your handler you'll
    have to change up your tests.  It's better to focus on the interaction
    with your handler and expected outputs.
    """
    fake = {'to': To}
    state_key = routing.Router.state_key(module, fake)
    assert_equal(routing.Router.STATE_STORE.get(state_key, From), state)


�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/utils.py���������������������������������������������������������������������0000644�0000765�0000024�00000007470�11311012457�016052� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
Mostly utility functions Lamson uses internally that don't
really belong anywhere else in the modules.  This module
is kind of a dumping ground, so if you find something that
can be improved feel free to work up a patch.
"""

from lamson import server, routing
import sys, os
import logging
import daemon
from daemon import pidlockfile
import imp
import signal


def import_settings(boot_also, from_dir=None, boot_module="config.boot"):
    """Used to import the settings in a Lamson project."""
    if from_dir:
        sys.path.append(from_dir)

    settings = __import__("config.settings", globals(), locals()).settings

    if boot_also:
        __import__(boot_module, globals(), locals())

    return settings


def daemonize(pid, chdir, chroot, umask, files_preserve=None, do_open=True):
    """
    Uses python-daemonize to do all the junk needed to make a
    server a server.  It supports all the features daemonize
    has, except that chroot probably won't work at all without
    some serious configuration on the system.
    """
    context = daemon.DaemonContext()
    context.pidfile = pidlockfile.PIDLockFile(pid)
    context.stdout = open(os.path.join(chdir, "logs/lamson.out"),"a+")                                                                                                       
    context.stderr = open(os.path.join(chdir, "logs/lamson.err"),"a+")                                                                                                       
    context.files_preserve = files_preserve or []
    context.working_directory = os.path.expanduser(chdir)

    if chroot: 
        context.chroot_directory = os.path.expanduser(chroot)
    if umask != False:
        context.umask = umask

    if do_open:
        context.open()

    return context

def drop_priv(uid, gid):
    """
    Changes the uid/gid to the two given, you should give utils.daemonize
    0,0 for the uid,gid so that it becomes root, which will allow you to then
    do this.
    """
    logging.debug("Dropping to uid=%d, gid=%d", uid, gid)
    daemon.daemon.change_process_owner(uid, gid)
    logging.debug("Now running as uid=%d, gid=%d", os.getgid(), os.getuid())



def make_fake_settings(host, port):
    """
    When running as a logging server we need a fake settings module to work with
    since the logging server can be run in any directory, so there may not be
    a config/settings.py file to import.
    """
    logging.basicConfig(filename="logs/logger.log", level=logging.DEBUG)
    routing.Router.load(['lamson.handlers.log', 'lamson.handlers.queue'])
    settings = imp.new_module('settings')
    settings.receiver = server.SMTPReceiver(host, port)
    settings.relay = None
    logging.info("Logging mode enabled, will not send email to anyone, just log.")

    return settings

def check_for_pid(pid, force):
    """Checks if a pid file is there, and if it is sys.exit.  If force given
    then it will remove the file and not exit if it's there."""
    if os.path.exists(pid):
        if not force:
            print "PID file %s exists, so assuming Lamson is running.  Give -FORCE to force it to start." % pid
            sys.exit(1)
            return # for unit tests mocking sys.exit
        else:
            os.unlink(pid)


def start_server(pid, force, chroot, chdir, uid, gid, umask, settings_loader):
    """
    Starts the server by doing a daemonize and then dropping priv
    accordingly.  It will only drop to the uid/gid given if both are given.
    """
    check_for_pid(pid, force)

    daemonize(pid, chdir, chroot, umask, files_preserve=[])

    sys.path.append(os.getcwd())

    settings = settings_loader()

    if uid and gid:
        drop_priv(uid, gid) 
    elif uid or gid:
        logging.warning("You probably meant to give a uid and gid, but you gave: uid=%r, gid=%r.  Will not change to any user.", uid, gid)

    settings.receiver.start()
��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/version.py�������������������������������������������������������������������0000644�0000765�0000024�00000000142�11313464560�016374� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VERSION={'version': '1.0pre11', 'rev': ['86905426c5', '86905426c5ef52aabd0ea4300f1bf54d204bcaf3']}������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson/view.py����������������������������������������������������������������������0000644�0000765�0000024�00000006343�11234771032�015667� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""
These are helper functions that make it easier to work with either
Jinja2 or Mako templates.  You MUST configure it by setting
lamson.view.LOADER to one of the template loaders in your config.boot
or config.testing.

After that these functions should just work.
"""

from lamson import mail
import email
import warnings

LOADER = None

def load(template):
    """
    Uses the registered loader to load the template you ask for.
    It assumes that your loader works like Jinja2 or Mako in that
    it has a LOADER.get_template() method that returns the template.
    """
    assert LOADER, "You haven't set lamson.view.LOADER to a loader yet."
    return LOADER.get_template(template)


def render(variables, template):
    """
    Takes the variables given and renders the template for you.
    Assumes the template returned by load() will have a .render()
    method that takes the variables as a dict.

    Use this if you just want to render a single template and don't
    want it to be a message.  Use render_message if the contents
    of the template are to be interpreted as a message with headers
    and a body.
    """
    return load(template).render(variables)


def respond(variables, Body=None, Html=None, **kwd):
    """
    Does the grunt work of cooking up a MailResponse that's based
    on a template.  The only difference from the lamson.mail.MailResponse
    class and this (apart from variables passed to a template) are that
    instead of giving actual Body or Html parameters with contents,
    you give the name of a template to render.  The kwd variables are
    the remaining keyword arguments to MailResponse of From/To/Subject.

    For example, to render a template for the body and a .html for the Html
    attachment, and to indicate the From/To/Subject do this:

        msg = view.respond(locals(), Body='template.txt', 
                          Html='template.html',
                          From='test@test.com',
                          To='receiver@test.com',
                          Subject='Test body from "%(dude)s".')

    In this case you're using locals() to gather the variables needed for
    the 'template.txt' and 'template.html' templates.  Each template is
    setup to be a text/plain or text/html attachment.  The From/To/Subject
    are setup as needed.  Finally, the locals() are also available as
    simple Python keyword templates in the From/To/Subject so you can pass
    in variables to modify those when needed (as in the %(dude)s in Subject).
    """

    assert Body or Html, "You need to give either the Body or Html template of the mail."

    for key in kwd:
        kwd[key] = kwd[key] % variables
    
    msg = mail.MailResponse(**kwd)

    if Body:
        msg.Body = render(variables, Body)
    
    if Html:
        msg.Html = render(variables, Html)

    return msg


def attach(msg, variables, template, filename=None, content_type=None,
           disposition=None):
    """
    Useful for rendering an attachment and then attaching it to the message
    given.  All the parameters that are in lamson.mail.MailResponse.attach
    are there as usual.
    """
    data = render(variables, template)

    msg.attach(filename=filename, data=data, content_type=content_type,
               disposition=disposition)

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson.egg-info/��������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�016037� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson.egg-info/dependency_links.txt������������������������������������������������0000644�0000765�0000024�00000000001�11313464570�022101� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson.egg-info/PKG-INFO������������������������������������������������������������0000644�0000765�0000024�00000000530�11313464570�017126� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Metadata-Version: 1.0
Name: lamson
Version: 1.0pre11
Summary: Lamson is a modern Pythonic mail server built like a web application server.
Home-page: http://pypi.python.org/pypi/lamson
Author: Zed A. Shaw
Author-email: zedshaw@zedshaw.com
License: UNKNOWN
Download-URL: http://pypi.python.org/pypi/lamson
Description: UNKNOWN
Platform: UNKNOWN
������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson.egg-info/requires.txt��������������������������������������������������������0000644�0000765�0000024�00000000046�11313464570�020433� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������chardet
jinja2
mock
nose
python-daemon������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/lamson.egg-info/SOURCES.txt���������������������������������������������������������0000644�0000765�0000024�00000046733�11313464570�017734� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������LICENSE
MANIFEST.in
README
build.vel
setup.py
bin/lamson
doc/lamsonproject.org/ChangeLog
doc/lamsonproject.org/Session.vim
doc/lamsonproject.org/build.vel
doc/lamsonproject.org/config.py
doc/lamsonproject.org/template.html
doc/lamsonproject.org/webgen.py
doc/lamsonproject.org/input/about.txt
doc/lamsonproject.org/input/contact.txt
doc/lamsonproject.org/input/download.txt
doc/lamsonproject.org/input/home_template.html
doc/lamsonproject.org/input/index.txt
doc/lamsonproject.org/input/blog/2009-05-16.txt
doc/lamsonproject.org/input/blog/2009-05-18.txt
doc/lamsonproject.org/input/blog/2009-05-19.txt
doc/lamsonproject.org/input/blog/2009-05-20.txt
doc/lamsonproject.org/input/blog/2009-05-24.txt
doc/lamsonproject.org/input/blog/2009-05-28.txt
doc/lamsonproject.org/input/blog/2009-05-31.txt
doc/lamsonproject.org/input/blog/2009-06-01.txt
doc/lamsonproject.org/input/blog/2009-06-03-2.txt
doc/lamsonproject.org/input/blog/2009-06-03.txt
doc/lamsonproject.org/input/blog/2009-06-04.txt
doc/lamsonproject.org/input/blog/2009-06-06.txt
doc/lamsonproject.org/input/blog/2009-06-08.txt
doc/lamsonproject.org/input/blog/2009-06-09.txt
doc/lamsonproject.org/input/blog/2009-06-14.txt
doc/lamsonproject.org/input/blog/2009-06-20.txt
doc/lamsonproject.org/input/blog/2009-06-22.txt
doc/lamsonproject.org/input/blog/2009-06-26.txt
doc/lamsonproject.org/input/blog/2009-07-03.txt
doc/lamsonproject.org/input/blog/2009-07-07.txt
doc/lamsonproject.org/input/blog/2009-07-09.txt
doc/lamsonproject.org/input/blog/2009-07-14.txt
doc/lamsonproject.org/input/blog/2009-07-19.txt
doc/lamsonproject.org/input/blog/2009-07-20.txt
doc/lamsonproject.org/input/blog/2009-08-03.txt
doc/lamsonproject.org/input/blog/2009-08-22.txt
doc/lamsonproject.org/input/blog/2009-09-07.txt
doc/lamsonproject.org/input/blog/2009-09-26.txt
doc/lamsonproject.org/input/blog/2009-12-12.txt
doc/lamsonproject.org/input/blog/2009-12-13.txt
doc/lamsonproject.org/input/blog/html_email_in_gmail.png
doc/lamsonproject.org/input/blog/index.txt
doc/lamsonproject.org/input/blog/template.html
doc/lamsonproject.org/input/docs/bounce_detection.txt
doc/lamsonproject.org/input/docs/confirmations.txt
doc/lamsonproject.org/input/docs/deferred_processing_to_queues.txt
doc/lamsonproject.org/input/docs/deploying_lamson.txt
doc/lamsonproject.org/input/docs/deploying_lamson_level_2.txt
doc/lamsonproject.org/input/docs/deploying_librelist.txt
doc/lamsonproject.org/input/docs/deploying_oneshotblog.txt
doc/lamsonproject.org/input/docs/faq.txt
doc/lamsonproject.org/input/docs/filtering_spam.txt
doc/lamsonproject.org/input/docs/getting_started.txt
doc/lamsonproject.org/input/docs/hooking_into_django.txt
doc/lamsonproject.org/input/docs/html_email_generation.txt
doc/lamsonproject.org/input/docs/index.txt
doc/lamsonproject.org/input/docs/introduction_to_finite_state_machines.txt
doc/lamsonproject.org/input/docs/lamson_commands.txt
doc/lamsonproject.org/input/docs/lamson_virtual_env.txt
doc/lamsonproject.org/input/docs/primary_vs_secondary_registration.txt
doc/lamsonproject.org/input/docs/unicode_encoding_and_decoding.txt
doc/lamsonproject.org/input/docs/unit_testing.txt
doc/lamsonproject.org/input/docs/writing_a_state_storage.txt
doc/lamsonproject.org/input/lists/index.txt
doc/lamsonproject.org/input/releases/index.txt
doc/lamsonproject.org/input/videos/index.txt
doc/lamsonproject.org/output/about.html
doc/lamsonproject.org/output/about.txt
doc/lamsonproject.org/output/contact.html
doc/lamsonproject.org/output/contact.txt
doc/lamsonproject.org/output/download.html
doc/lamsonproject.org/output/download.txt
doc/lamsonproject.org/output/favicon.ico
doc/lamsonproject.org/output/feed.xml
doc/lamsonproject.org/output/home_template.html
doc/lamsonproject.org/output/index.html
doc/lamsonproject.org/output/index.txt
doc/lamsonproject.org/output/mailocalypse.py
doc/lamsonproject.org/output/prettify.css
doc/lamsonproject.org/output/prettify.js
doc/lamsonproject.org/output/blog/2009-05-16.html
doc/lamsonproject.org/output/blog/2009-05-16.txt
doc/lamsonproject.org/output/blog/2009-05-18.html
doc/lamsonproject.org/output/blog/2009-05-18.txt
doc/lamsonproject.org/output/blog/2009-05-19.html
doc/lamsonproject.org/output/blog/2009-05-19.txt
doc/lamsonproject.org/output/blog/2009-05-20.html
doc/lamsonproject.org/output/blog/2009-05-20.txt
doc/lamsonproject.org/output/blog/2009-05-24.html
doc/lamsonproject.org/output/blog/2009-05-24.txt
doc/lamsonproject.org/output/blog/2009-05-28.html
doc/lamsonproject.org/output/blog/2009-05-28.txt
doc/lamsonproject.org/output/blog/2009-05-31.html
doc/lamsonproject.org/output/blog/2009-05-31.txt
doc/lamsonproject.org/output/blog/2009-06-01.html
doc/lamsonproject.org/output/blog/2009-06-01.txt
doc/lamsonproject.org/output/blog/2009-06-03-2.html
doc/lamsonproject.org/output/blog/2009-06-03-2.txt
doc/lamsonproject.org/output/blog/2009-06-03.html
doc/lamsonproject.org/output/blog/2009-06-03.txt
doc/lamsonproject.org/output/blog/2009-06-04.html
doc/lamsonproject.org/output/blog/2009-06-04.txt
doc/lamsonproject.org/output/blog/2009-06-06.html
doc/lamsonproject.org/output/blog/2009-06-06.txt
doc/lamsonproject.org/output/blog/2009-06-08.html
doc/lamsonproject.org/output/blog/2009-06-08.txt
doc/lamsonproject.org/output/blog/2009-06-09.html
doc/lamsonproject.org/output/blog/2009-06-09.txt
doc/lamsonproject.org/output/blog/2009-06-14.html
doc/lamsonproject.org/output/blog/2009-06-14.txt
doc/lamsonproject.org/output/blog/2009-06-20.html
doc/lamsonproject.org/output/blog/2009-06-20.txt
doc/lamsonproject.org/output/blog/2009-06-22.html
doc/lamsonproject.org/output/blog/2009-06-22.txt
doc/lamsonproject.org/output/blog/2009-06-26.html
doc/lamsonproject.org/output/blog/2009-06-26.txt
doc/lamsonproject.org/output/blog/2009-07-03.html
doc/lamsonproject.org/output/blog/2009-07-03.txt
doc/lamsonproject.org/output/blog/2009-07-07.html
doc/lamsonproject.org/output/blog/2009-07-07.txt
doc/lamsonproject.org/output/blog/2009-07-09.html
doc/lamsonproject.org/output/blog/2009-07-09.txt
doc/lamsonproject.org/output/blog/2009-07-14.html
doc/lamsonproject.org/output/blog/2009-07-14.txt
doc/lamsonproject.org/output/blog/2009-07-19.html
doc/lamsonproject.org/output/blog/2009-07-19.txt
doc/lamsonproject.org/output/blog/2009-07-20.html
doc/lamsonproject.org/output/blog/2009-07-20.txt
doc/lamsonproject.org/output/blog/2009-08-03.html
doc/lamsonproject.org/output/blog/2009-08-03.txt
doc/lamsonproject.org/output/blog/2009-08-22.html
doc/lamsonproject.org/output/blog/2009-08-22.txt
doc/lamsonproject.org/output/blog/2009-09-07.html
doc/lamsonproject.org/output/blog/2009-09-07.txt
doc/lamsonproject.org/output/blog/2009-09-26.html
doc/lamsonproject.org/output/blog/2009-09-26.txt
doc/lamsonproject.org/output/blog/2009-12-12.html
doc/lamsonproject.org/output/blog/2009-12-12.txt
doc/lamsonproject.org/output/blog/2009-12-13.html
doc/lamsonproject.org/output/blog/2009-12-13.txt
doc/lamsonproject.org/output/blog/html_email_in_gmail.png
doc/lamsonproject.org/output/blog/index.html
doc/lamsonproject.org/output/blog/index.txt
doc/lamsonproject.org/output/blog/template.html
doc/lamsonproject.org/output/css/code.css
doc/lamsonproject.org/output/css/style.css
doc/lamsonproject.org/output/css/style_ie.css
doc/lamsonproject.org/output/docs/bounce_detection.html
doc/lamsonproject.org/output/docs/bounce_detection.txt
doc/lamsonproject.org/output/docs/confirmations.html
doc/lamsonproject.org/output/docs/confirmations.txt
doc/lamsonproject.org/output/docs/deferred_processing_to_queues.html
doc/lamsonproject.org/output/docs/deferred_processing_to_queues.txt
doc/lamsonproject.org/output/docs/deploying_lamson.html
doc/lamsonproject.org/output/docs/deploying_lamson.txt
doc/lamsonproject.org/output/docs/deploying_lamson_level_2.html
doc/lamsonproject.org/output/docs/deploying_lamson_level_2.txt
doc/lamsonproject.org/output/docs/deploying_librelist.html
doc/lamsonproject.org/output/docs/deploying_librelist.txt
doc/lamsonproject.org/output/docs/deploying_oneshotblog.html
doc/lamsonproject.org/output/docs/deploying_oneshotblog.txt
doc/lamsonproject.org/output/docs/faq.html
doc/lamsonproject.org/output/docs/faq.txt
doc/lamsonproject.org/output/docs/filtering_spam.html
doc/lamsonproject.org/output/docs/filtering_spam.txt
doc/lamsonproject.org/output/docs/getting_started.html
doc/lamsonproject.org/output/docs/getting_started.txt
doc/lamsonproject.org/output/docs/hooking_into_django.html
doc/lamsonproject.org/output/docs/hooking_into_django.txt
doc/lamsonproject.org/output/docs/html_email_generation.html
doc/lamsonproject.org/output/docs/html_email_generation.txt
doc/lamsonproject.org/output/docs/index.html
doc/lamsonproject.org/output/docs/index.txt
doc/lamsonproject.org/output/docs/introduction_to_finite_state_machines.html
doc/lamsonproject.org/output/docs/introduction_to_finite_state_machines.txt
doc/lamsonproject.org/output/docs/lamson_commands.html
doc/lamsonproject.org/output/docs/lamson_commands.txt
doc/lamsonproject.org/output/docs/lamson_virtual_env.html
doc/lamsonproject.org/output/docs/lamson_virtual_env.txt
doc/lamsonproject.org/output/docs/primary_vs_secondary_registration.html
doc/lamsonproject.org/output/docs/primary_vs_secondary_registration.txt
doc/lamsonproject.org/output/docs/template.html
doc/lamsonproject.org/output/docs/unicode_encoding_and_decoding.html
doc/lamsonproject.org/output/docs/unicode_encoding_and_decoding.txt
doc/lamsonproject.org/output/docs/unit_testing.html
doc/lamsonproject.org/output/docs/unit_testing.txt
doc/lamsonproject.org/output/docs/writing_a_state_storage.html
doc/lamsonproject.org/output/docs/writing_a_state_storage.txt
doc/lamsonproject.org/output/images/bg.gif
doc/lamsonproject.org/output/images/buttons.png
doc/lamsonproject.org/output/images/capbl.gif
doc/lamsonproject.org/output/images/capbr.gif
doc/lamsonproject.org/output/images/captl.gif
doc/lamsonproject.org/output/images/captr.gif
doc/lamsonproject.org/output/images/dashed.gif
doc/lamsonproject.org/output/images/gradient_bg.gif
doc/lamsonproject.org/output/images/lamson.png
doc/lamsonproject.org/output/images/li_dot1.gif
doc/lamsonproject.org/output/images/li_dot2.gif
doc/lamsonproject.org/output/images/menuact.gif
doc/lamsonproject.org/output/images/menubg.gif
doc/lamsonproject.org/output/images/menucapbr.gif
doc/lamsonproject.org/output/images/menucaptr.gif
doc/lamsonproject.org/output/images/metanavbg.gif
doc/lamsonproject.org/output/images/metanavbl.gif
doc/lamsonproject.org/output/images/metanavbr.gif
doc/lamsonproject.org/output/images/metanavtl.gif
doc/lamsonproject.org/output/images/metanavtr.gif
doc/lamsonproject.org/output/images/quotebg.gif
doc/lamsonproject.org/output/images/rss.png
doc/lamsonproject.org/output/images/smenubg.gif
doc/lamsonproject.org/output/images/smenucapbr.gif
doc/lamsonproject.org/output/images/smenucaptr.gif
doc/lamsonproject.org/output/images/testimg.gif
doc/lamsonproject.org/output/lists/index.html
doc/lamsonproject.org/output/lists/index.txt
doc/lamsonproject.org/output/releases/index.html
doc/lamsonproject.org/output/releases/index.txt
doc/lamsonproject.org/output/styles/global.css
doc/lamsonproject.org/output/styles/reset.css
doc/lamsonproject.org/output/videos/index.html
doc/lamsonproject.org/output/videos/index.txt
examples/librelist/README
examples/librelist/muttrc
examples/librelist/app/__init__.py
examples/librelist/app/handlers/__init__.py
examples/librelist/app/handlers/admin.py
examples/librelist/app/handlers/bounce.py
examples/librelist/app/model/__init__.py
examples/librelist/app/model/archive.py
examples/librelist/app/model/bounce.py
examples/librelist/app/model/confirmation.py
examples/librelist/app/model/mailinglist.py
examples/librelist/app/model/state_storage.py
examples/librelist/app/templates/mail/bad_list_name.msg
examples/librelist/app/templates/mail/confirmation.msg
examples/librelist/app/templates/mail/create_confirmation.msg
examples/librelist/app/templates/mail/subscribed.msg
examples/librelist/app/templates/mail/unbounce_confirm.msg
examples/librelist/app/templates/mail/unsubscribed.msg
examples/librelist/app/templates/mail/we_have_disabled_you.msg
examples/librelist/app/templates/mail/you_are_unbounced.msg
examples/librelist/app/templates/mail/you_bounced.msg
examples/librelist/config/__init__.py
examples/librelist/config/boot.py
examples/librelist/config/logging.conf
examples/librelist/config/settings.py
examples/librelist/config/test_logging.conf
examples/librelist/config/testing.py
examples/librelist/deploy/backup
examples/librelist/deploy/forward
examples/librelist/deploy/log4sh.properties
examples/librelist/deploy/rollback
examples/librelist/deploy/env/testing
examples/librelist/deploy/lib/log4sh
examples/librelist/deploy/lib/migrate
examples/librelist/deploy/lib/shunit2
examples/librelist/deploy/migrations/001
examples/librelist/deploy/migrations/002
examples/librelist/deploy/migrations/003
examples/librelist/deploy/migrations/004
examples/librelist/deploy/scripts/json_convert.py
examples/librelist/lib/__init__.py
examples/librelist/lib/metaphone.py
examples/librelist/tests/bounce.msg
examples/librelist/tests/lots_of_headers.msg
examples/librelist/tests/handlers/__init__.py
examples/librelist/tests/handlers/admin_tests.py
examples/librelist/tests/handlers/bounce_tests.py
examples/librelist/tests/model/__init__.py
examples/librelist/tests/model/archive_tests.py
examples/librelist/tests/model/bounce_tests.py
examples/librelist/tests/model/confirmation_tests.py
examples/librelist/tests/model/mailinglist_tests.py
examples/librelist/tests/model/state_storage_tests.py
examples/librelist/tests/templates/__init__.py
examples/librelist/webapp/__init__.py
examples/librelist/webapp/manage.py
examples/librelist/webapp/settings.py
examples/librelist/webapp/urls.py
examples/librelist/webapp/librelist/__init__.py
examples/librelist/webapp/librelist/admin.py
examples/librelist/webapp/librelist/models.py
examples/librelist/webapp/librelist/urls.py
examples/librelist/webapp/librelist/views.py
examples/librelist/webapp/librelist/migrations/0001_initial.py
examples/librelist/webapp/librelist/migrations/__init__.py
examples/myinboxisnotatv/README
examples/myinboxisnotatv/muttrc
examples/myinboxisnotatv/app/__init__.py
examples/myinboxisnotatv/app/handlers/__init__.py
examples/myinboxisnotatv/app/handlers/anonymizer.py
examples/myinboxisnotatv/app/model/__init__.py
examples/myinboxisnotatv/app/model/addressing.py
examples/myinboxisnotatv/app/model/filter.py
examples/myinboxisnotatv/app/model/html.py
examples/myinboxisnotatv/app/templates/mail/forbid.msg
examples/myinboxisnotatv/app/templates/mail/start_confirm.msg
examples/myinboxisnotatv/app/templates/mail/welcome.msg
examples/myinboxisnotatv/config/__init__.py
examples/myinboxisnotatv/config/boot.py
examples/myinboxisnotatv/config/logging.conf
examples/myinboxisnotatv/config/settings.py
examples/myinboxisnotatv/config/test_logging.conf
examples/myinboxisnotatv/config/testing.py
examples/myinboxisnotatv/deploy/backup
examples/myinboxisnotatv/deploy/forward
examples/myinboxisnotatv/deploy/log4sh.properties
examples/myinboxisnotatv/deploy/rollback
examples/myinboxisnotatv/deploy/env/testing
examples/myinboxisnotatv/deploy/lib/log4sh
examples/myinboxisnotatv/deploy/lib/migrate
examples/myinboxisnotatv/deploy/lib/shunit2
examples/myinboxisnotatv/deploy/scripts/json_convert.py
examples/myinboxisnotatv/tests/index.html
examples/myinboxisnotatv/tests/handlers/__init__.py
examples/myinboxisnotatv/tests/handlers/anonymizer_tests.py
examples/myinboxisnotatv/tests/model/__init__.py
examples/myinboxisnotatv/tests/model/addressing_tests.py
examples/myinboxisnotatv/tests/model/filter_tests.py
examples/myinboxisnotatv/tests/model/html_tests.py
examples/myinboxisnotatv/tests/templates/__init__.py
examples/osb/README
examples/osb/muttrc
examples/osb/pendingrc
examples/osb/app/__init__.py
examples/osb/app/data/about.html
examples/osb/app/data/help.html
examples/osb/app/data/jquery.js
examples/osb/app/data/spam.html
examples/osb/app/data/styles/main.css
examples/osb/app/data/styles/reset.css
examples/osb/app/handlers/__init__.py
examples/osb/app/handlers/comment.py
examples/osb/app/handlers/index.py
examples/osb/app/handlers/post.py
examples/osb/app/model/__init__.py
examples/osb/app/model/comment.py
examples/osb/app/model/post.py
examples/osb/app/templates/mail/comment_confirm.msg
examples/osb/app/templates/mail/comment_submitted.msg
examples/osb/app/templates/mail/confirm.msg
examples/osb/app/templates/mail/deleted.msg
examples/osb/app/templates/mail/page_ready.msg
examples/osb/app/templates/mail/welcome.msg
examples/osb/app/templates/web/base.html
examples/osb/app/templates/web/comments.html
examples/osb/app/templates/web/index.html
examples/osb/app/templates/web/post.html
examples/osb/config/__init__.py
examples/osb/config/boot.py
examples/osb/config/forward.py
examples/osb/config/logging.conf
examples/osb/config/queue.py
examples/osb/config/settings.py
examples/osb/config/test_logging.conf
examples/osb/config/testing.py
examples/osb/doc/done.txt
examples/osb/doc/report.txt
examples/osb/doc/todo.txt
examples/osb/tests/spam
examples/osb/tests/handlers/__init__.py
examples/osb/tests/handlers/comments_tests.py
examples/osb/tests/handlers/index_tests.py
examples/osb/tests/handlers/post_tests.py
examples/osb/tests/model/__init__.py
examples/osb/tests/model/comment.py
examples/osb/tests/model/post_tests.py
examples/osb/tests/templates/__init__.py
examples/osb/tests/templates/osb_tests.py
lamson/__init__.py
lamson/args.py
lamson/bounce.py
lamson/commands.py
lamson/confirm.py
lamson/encoding.py
lamson/html.py
lamson/mail.py
lamson/queue.py
lamson/routing.py
lamson/server.py
lamson/spam.py
lamson/testing.py
lamson/utils.py
lamson/version.py
lamson/view.py
lamson.egg-info/PKG-INFO
lamson.egg-info/SOURCES.txt
lamson.egg-info/dependency_links.txt
lamson.egg-info/requires.txt
lamson.egg-info/top_level.txt
lamson/data/prototype.zip
lamson/data/prototype/README
lamson/data/prototype/muttrc
lamson/data/prototype/app/__init__.py
lamson/data/prototype/app/handlers/__init__.py
lamson/data/prototype/app/handlers/sample.py
lamson/data/prototype/app/model/__init__.py
lamson/data/prototype/config/__init__.py
lamson/data/prototype/config/boot.py
lamson/data/prototype/config/logging.conf
lamson/data/prototype/config/settings.py
lamson/data/prototype/config/test_logging.conf
lamson/data/prototype/config/testing.py
lamson/data/prototype/tests/handlers/__init__.py
lamson/data/prototype/tests/handlers/open_relay_tests.py
lamson/data/prototype/tests/model/__init__.py
lamson/data/prototype/tests/templates/__init__.py
lamson/handlers/__init__.py
lamson/handlers/forward.py
lamson/handlers/log.py
lamson/handlers/queue.py
scripts/dist.vel
scripts/sample.vel
scripts/setup.py
scripts/testing.vel
tests/borked.msg
tests/bounce.msg
tests/lamson.png
tests/signed.msg
tests/spam
tests/statesdb.db
tests/config/__init__.py
tests/config/logging.conf
tests/config/settings.py
tests/config/testing.py
tests/lamson_tests/__init__.py
tests/lamson_tests/args_tests.py
tests/lamson_tests/bounce_filtered_mod.py
tests/lamson_tests/bounce_tests.py
tests/lamson_tests/command_tests.py
tests/lamson_tests/confirm_tests.py
tests/lamson_tests/encoding_tests.py
tests/lamson_tests/handler_tests.py
tests/lamson_tests/html_tests.py
tests/lamson_tests/message_tests.py
tests/lamson_tests/queue_tests.py
tests/lamson_tests/routing_tests.py
tests/lamson_tests/server_tests.py
tests/lamson_tests/simple_fsm_mod.py
tests/lamson_tests/spam_filtered_mod.py
tests/lamson_tests/spam_tests.py
tests/lamson_tests/testing_tests.py
tests/lamson_tests/utils_tests.py
tests/lamson_tests/view_tests.py
tests/lamson_tests/templates/confirmation.msg
tests/lamson_tests/templates/content.markdown
tests/lamson_tests/templates/html_test.html
tests/lamson_tests/templates/style.css
tests/lamson_tests/templates/template.html
tests/lamson_tests/templates/template.txt
tests/lamson_tests/templates/unicode.html�������������������������������������lamson-1.0pre11/lamson.egg-info/top_level.txt�������������������������������������������������������0000644�0000765�0000024�00000000007�11313464570�020562� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/LICENSE�����������������������������������������������������������������������������0000644�0000765�0000024�00000077332�11201004224�014047� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS


������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/MANIFEST.in�������������������������������������������������������������������������0000644�0000765�0000024�00000000330�11203336365�014600� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������include *.py
include *.vel
include LICENSE
include README
recursive-include examples *
recursive-include doc *
recursive-include bin *
recursive-include lamson *
recursive-include scripts *
recursive-include tests *
��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/PKG-INFO����������������������������������������������������������������������������0000644�0000765�0000024�00000000530�11313464575�014150� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Metadata-Version: 1.0
Name: lamson
Version: 1.0pre11
Summary: Lamson is a modern Pythonic mail server built like a web application server.
Home-page: http://pypi.python.org/pypi/lamson
Author: Zed A. Shaw
Author-email: zedshaw@zedshaw.com
License: UNKNOWN
Download-URL: http://pypi.python.org/pypi/lamson
Description: UNKNOWN
Platform: UNKNOWN
������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/README������������������������������������������������������������������������������0000644�0000765�0000024�00000014073�11226534017�013732� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������The Lamson Mail Server(TM)

======================

Lamson is a pure Python SMTP server designed to create robust and complex mail
applications in the style of modern web frameworks such as Django. Unlike
traditional SMTP servers like Postfix or Sendmail, Lamson has all the features
of a web application stack (ORM, templates, routing, handlers, state machines,
Python) without needing to configure alias files, run newaliases, or
juggle tons of tiny fragile processes. Lamson also plays well with other web
frameworks and Python libraries.

Features
========

Lamson supports running in many contexts for processing mail using the best
technology currently available.  Since Lamson is aiming to be a modern SMTP
server and Mail processing framework, it has some features you don't find in any
other Mail server.

* Written in portable Python that should run on almost any Unix server.
* Handles mail in almost any encoding and format, including attachments, and
canonicalizes them for easier processing.
* Sends nearly pristine clean mail that is easier to process by other receiving
servers.
* Properly decodes internationalized mail into Python unicode, and translates
Python unicode back into nice clean ascii and/or UTF-8 mail.
* Lamson can use SQLAlchemy, TokyoCabinet, or any other database abstraction
layer or technology you can get libraries for.  It supports SQLAlchemy by
default.
* It uses Jinja2 by default, but you can swap in Mako if you like, or any other
template framework with a similar API.
* Supports working with Maildir queues to defer work and distribute it to
multiple machines.
* Can run as an non-root user on port 25 to reduce the risk of intrusion.
* Lamson can also run in a completely separate virtualenv for easy deployment.
* Spam filtering is baked into Lamson using the SpamBayes library.
* A flexible and easy to use routing system lets you write stateful or stateLESS
handlers of your email.
* Helpful tools for unit testing your email applications with nose, including
spell checking with PyEnchant.
* Ability to use Jinja2 or Mako templates to craft emails including the headers.
* A full alternative to the default optparse library for doing commands easily.
* Easily configurable to use alternative sending and receiving systems, database
libraries, or any other systems you need to talk to.
* Yet, you don't *have* to configure everything to get stated.  A simple
lamson gen command lets you get an application up and running quick.
* Finally, many helpful commands for general SMTP server debugging and cleaning.


Installing
==========

If you want to install Lamson then the best source of information is the
documentation available at the site, particularly the following documents:

http://lamsonproject.org/docs/getting_started.html

http://lamsonproject.org/docs/deploying_lamson.html


Project Information
===================

You can get documentation, track news, watch screencasts (using actual GNU screen)
and other information at:

http://lamsonproject.org/

Source
-----

If you want to get the source then you can use Bazaar:

bzr branch lp:lamson

Bazaar may ask you to login, but it should still give you the source.


Status
------

Lamson is currently nearing the 1.0 stage, with almost everything you need to
build an email based application server.  The source is well documented, has
nearly full test coverage, and runs on Python 2.5 and 2.6.


License
----

Lamson is released under the GNU GPLv3 license, which should be included with
your copy of the source code.  If you didn't receive a copy of the license then
you didn't get the right version of the source code.


Contributing
-------

Most of the features for 1.0 are already being planned, but if you have
suggestions for improvements or bug fixes then feel free to join the mailing
lists and discuss them:

http://lamsonproject.org/lists/

There is also an IRC channel #lamson on irc.freenode.org you can join to chat,
it has low volume and you usually get a fast response.


Testing
=======

The Lamson project uses unit tests, code reviews, coverage information, source
analysis, and security reviews to maintain quality.  If you find a bug, please
take the time to write a test case that fails or provide a piece of mail that
causes the failure.

If you contribute new code then your code should have as much coverage as
possible, with a minimal amount of mocking.


Security
--------

Lamson follows the same security reporting model that has worked for other open
source projects:  If you report a security vulnerability, it will be acted on
immediately and a fix with complete full disclosure will go out to everyone at
the same time.  It's the job of the people using Lamson to keep track of
security relate problems.

Additionally, Lamson is written in as secure a manner as possible and assumes
that it is operating in a hostile environment.  If you find Lamson doesn't
behave correctly given that constraint then please voice your concerns.



Development
===========

Lamson is written entirely in Python and runs on Python 2.5 or 2.6 but not 3k
yet.  It uses only pure Python except where some libraries have compiled
extensions (such as Jinja2).  It should hopefully run on any platform that
supports Python and has Unix semantics.

The code is consistently documented and written to be read in an instructional
manner where possible.  If a piece of code does not make sense, then ask for
help and it will be clarified.  The code is also small and has a full test suite
with about 95% coverage, so you should be able to find out just about anything
you need to hack on Lamson in the Lamson source.  Particularly you can find
online API documentation here:

http://lamsonproject.org/docs/api/

Given the above statements, it should be possible for anyone to take the Lamson
source and read through it in an evening or two.  You should also be able to
understand what's going on, and learn anything you don't by asking questions.

If this isn't the case, then feel free to ask for help understanding it.


���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/scripts/����������������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�014543� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/scripts/dist.vel��������������������������������������������������������������������0000644�0000765�0000024�00000001710�11165520647�016214� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# requires that you have options:  setup, project, website, vesion, and sudo

depends (
        config ['dist.clean' 'dist.gen.setup'] 
        install ['dist.config']
        sdist ['dist.config']
        release ['dist.sdist']
)

targets (
        config 
            $ python setup.py config

        sdist [
            $ rm -f MANIFEST
            $ python setup.py sdist
        ]

        install [
           $ %(sudo)s python setup.py install
           $ %(sudo)s chown -R `whoami` build dist %(project)s.egg-info
        ]

        clean [
            $ %(sudo)s rm -rf build dist %(project)s.egg-info
            $ %(sudo)s rm -f `find . -name "*.pyc"`
        ]

        gen.setup 
            gen(input 'scripts/setup.py' output 'setup.py')

        cheese [
            needs ['dist.sdist']
            $ python setup.py register bdist_egg upload
        ]

        release [
            $ cp dist/%(project)s-%(version)s.tar.gz %(website)s
        ]
)
��������������������������������������������������������lamson-1.0pre11/scripts/sample.vel������������������������������������������������������������������0000644�0000765�0000024�00000000415�11165520647�016533� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������targets(
        commands [
            $ echo noop > /dev/null
            log "Testing out the commands and stuff."
            given "True"
            py 'print "hi!"'
            $ rm -f setup.py
            gen (from 'scripts/setup.py' to 'setup.py')
        ]
)
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/scripts/setup.py��������������������������������������������������������������������0000644�0000765�0000024�00000000337�11165520647�016257� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������## this file is generated from settings in build.vel

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

# from options["setup"] in build.vel
config = %(setup)s
setup(**config)

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/scripts/testing.vel�����������������������������������������������������������������0000644�0000765�0000024�00000000125�11165520647�016725� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������targets(
    run 'nosetests'
    verbose 'nosetests -s'
    noop py 'print "test"'
)
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/setup.cfg���������������������������������������������������������������������������0000644�0000765�0000024�00000000073�11313464575�014676� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[egg_info]
tag_build = 
tag_date = 0
tag_svn_revision = 0

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/setup.py����������������������������������������������������������������������������0000644�0000765�0000024�00000001307�11313464562�014564� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������## this file is generated from settings in build.vel

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

# from options["setup"] in build.vel
config = {'package_data': {'lamson': ['data/prototype.zip']}, 'description': 'Lamson is a modern Pythonic mail server built like a web application server.', 'author': 'Zed A. Shaw', 'url': 'http://pypi.python.org/pypi/lamson', 'download_url': 'http://pypi.python.org/pypi/lamson', 'author_email': 'zedshaw@zedshaw.com', 'version': '1.0pre11', 'scripts': ['bin/lamson'], 'install_requires': ['chardet', 'jinja2', 'mock', 'nose', 'python-daemon'], 'packages': ['lamson', 'lamson.handlers'], 'name': 'lamson'}
setup(**config)

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/������������������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464574�014216� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/borked.msg��������������������������������������������������������������������0000644�0000765�0000024�00000005247�11230257624�016176� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Received: from bazar2.conectiva.com.br (bazar2.conectiva.com.br [127.0.0.1])
	by bazar2.conectiva.com.br (Postfix) with ESMTP id D906B18AD6;
	Sun, 21 Jun 2009 17:42:54 -0300 (BRT)
X-Original-To: lua@bazar2.conectiva.com.br
Delivered-To: lua@bazar2.conectiva.com.br
Received: from smtp-out-01.simnet.is (smtp-out-01.simnet.is [194.105.231.27])
	by bazar2.conectiva.com.br (Postfix) with ESMTP id E2C9118A1B
	for <lua@bazar2.conectiva.com.br>; Sun, 21 Jun 2009 17:42:50 -0300 (BRT)
Authentication-Results: smtp-out-01.simnetpro.is;
	dkim=neutral (message not signed) header.i=none
X-SBRS: 0.8
X-IronPort-Anti-Spam-Filtered: true
X-IronPort-Anti-Spam-Result: Au4KAKo4PkrCaedFbGdsb2JhbACYXwsOCQcTswaECgU
X-IronPort-AV: E=Sophos;i="4.42,264,1243814400"; d="scan'208";a="222914853"
Received: from consumer-mta-01.simnet.is ([194.105.231.69])
	by smtp-out-01.simnetpro.is with ESMTP; 21 Jun 2009 20:42:38 +0000
Subject: Re: Problem Integrating Lua with Visual Studio 2005
From: =?ISO-8859-1?Q?Gabr=EDel?= "A." =?ISO-8859-1?Q?P=E9tursson?=
	<gabrielp@simnet.is>
To: Lua list <lua@bazar2.conectiva.com.br>
In-Reply-To: <dbaa5cbe0906211336uf58d390r94f5da3017d0fd5c@mail.gmail.com>
References: <dbaa5cbe0906211336uf58d390r94f5da3017d0fd5c@mail.gmail.com>
Content-Type: text/plain
Date: Sun, 21 Jun 2009 20:42:38 +0000
Message-Id: <1245616958.16548.2.camel@debian.lan>
Mime-Version: 1.0
X-Mailer: Evolution 2.26.1.1 
Content-Transfer-Encoding: 7bit
X-BeenThere: lua@bazar2.conectiva.com.br
X-Mailman-Version: 2.1.6
Precedence: list
Reply-To: Lua list <lua@bazar2.conectiva.com.br>
List-Id: Lua list <lua.bazar2.conectiva.com.br>
List-Unsubscribe: <http://bazar2.conectiva.com.br/mailman/listinfo/lua>,
	<mailto:lua-request@bazar2.conectiva.com.br?subject=unsubscribe>
List-Archive: <http://bazar2.conectiva.com.br/mailman/private/lua>
List-Post: <mailto:lua@bazar2.conectiva.com.br>
List-Help: <mailto:lua-request@bazar2.conectiva.com.br?subject=help>
List-Subscribe: <http://bazar2.conectiva.com.br/mailman/listinfo/lua>,
	<mailto:lua-request@bazar2.conectiva.com.br?subject=subscribe>
Sender: lua-bounces@bazar2.conectiva.com.br
Errors-To: lua-bounces@bazar2.conectiva.com.br
X-Spambayes-Classification: ham; 0.00

You may start by telling us what kind or errors you are experiencing.

On Sun, 2009-06-21 at 16:36 -0400, shanthan raj wrote:
> Hi,
> 
> I'm new to Lua. I want to develop a game using Lua. I'm planning to
> use Visual Studio 2005 for the development. I have downloaded [VS Lua
> Language Pack] (5.1) . When a run a simple program in Lua using Visual
> Studio, I'm getting some errors. May be I'm doing something wrong. Can
> anybody guide how to start with a simple "Hello World" program.
> 
> Thanks in Advance,
> 
> Raj
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/bounce.msg��������������������������������������������������������������������0000600�0000765�0000024�00000006143�11225443162�016165� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������From MAILER-DAEMON  Tue Jul  7 22:47:39 2009
Return-Path: <>
X-Original-To: zedshaw@zedshaw.com
Delivered-To: zedshaw@zedshaw.com
Received: by mail.zedshaw.com (Postfix)
	id 270231E8B13; Tue,  7 Jul 2009 22:47:39 -0700 (PDT)
Date: Tue,  7 Jul 2009 22:47:39 -0700 (PDT)
From: MAILER-DAEMON@zedshaw.com (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: zedshaw@zedshaw.com
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
	boundary="C99B61E8B12.1247032059/mail.zedshaw.com"
Message-Id: <20090708054739.270231E8B13@mail.zedshaw.com>
Status: RO

This is a MIME-encapsulated message.

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Notification
Content-Type: text/plain; charset=us-ascii

This is the mail system at host mail.zedshaw.com.

I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.

For further assistance, please send mail to postmaster.

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

                   The mail system

<asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com>: host
    gmail-smtp-in.l.google.com[209.85.210.17] said: 550-5.1.1 The email account
    that you tried to reach does not exist. Please try 550-5.1.1
    double-checking the recipient's email address for typos or 550-5.1.1
    unnecessary spaces. Learn more at                              550 5.1.1
    http://mail.google.com/support/bin/answer.py?answer=6596 17si20661415yxe.22
    (in reply to RCPT TO command)

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Delivery report
Content-Type: message/delivery-status

Reporting-MTA: dns; mail.zedshaw.com
X-Postfix-Queue-ID: C99B61E8B12
X-Postfix-Sender: rfc822; zedshaw@zedshaw.com
Arrival-Date: Tue,  7 Jul 2009 22:47:35 -0700 (PDT)

Final-Recipient: rfc822; asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
Action: failed
Status: 5.1.1
Remote-MTA: dns; gmail-smtp-in.l.google.com
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does
    not exist. Please try 550-5.1.1 double-checking the recipient's email
    address for typos or 550-5.1.1 unnecessary spaces. Learn more at
    550 5.1.1 http://mail.google.com/support/bin/answer.py?answer=6596
    17si20661415yxe.22

--C99B61E8B12.1247032059/mail.zedshaw.com
Content-Description: Undelivered Message
Content-Type: message/rfc822

Return-Path: <zedshaw@zedshaw.com>
Received: by mail.zedshaw.com (Postfix, from userid 1000)
	id C99B61E8B12; Tue,  7 Jul 2009 22:47:35 -0700 (PDT)
Date: Tue, 7 Jul 2009 22:47:35 -0700
From: "Zed A. Shaw" <zedshaw@zedshaw.com>
To: asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com
Subject: test bounce
Message-ID: <20090708054735.GC7578@zedshaw>
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
User-Agent: Mutt/1.5.18 (2008-05-17)

If this is a real address, then sorry.  I'm trying to get some bounces.

-- 
Zed A. Shaw
http://zedshaw.com/

--C99B61E8B12.1247032059/mail.zedshaw.com--
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/config/�����������������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464575�015464� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/config/__init__.py������������������������������������������������������������0000644�0000765�0000024�00000000000�11212222650�017543� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/config/logging.conf�����������������������������������������������������������0000644�0000765�0000024�00000001042�11212223504�017735� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[loggers]
keys=root,routing

[handlers]
keys=stdoutHandler,stderrHandler

[formatters]
keys=defaultFormatter

[logger_root]
level=DEBUG
handlers=stdoutHandler

[logger_routing]
level=DEBUG
handlers=stdoutHandler
qualname=routing
propagate=0

[handler_stdoutHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)

[handler_stderrHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stderr,)

[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/config/settings.py������������������������������������������������������������0000644�0000765�0000024�00000000730�11251311713�017660� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file contains python variables that configure Lamson for email processing.
import logging

relay_config = {'host': 'localhost', 'port': 8825}

receiver_config = {'host': 'localhost', 'port': 8823}

handlers = []

router_defaults = {'host': 'localhost'}

template_config = {'dir': 'lamson_tests', 'module': '.'}

BLOG_BASE="app/data/posts"

# this is for when you run the config.queue boot
queue_config = {'queue': 'run/deferred', 'sleep': 10}

queue_handlers = []

����������������������������������������lamson-1.0pre11/tests/config/testing.py�������������������������������������������������������������0000644�0000765�0000024�00000002065�11227341366�017512� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from config import settings
from lamson import view
from lamson.routing import Router
from lamson.server import Relay
import jinja2
import logging
import logging.config
import os

# configure logging to go to a log file
logging.config.fileConfig("tests/config/logging.conf")

# the relay host to actually send the final message to (set debug=1 to see what
# the relay is saying to the log server).
settings.relay = Relay(host=settings.relay_config['host'], 
                       port=settings.relay_config['port'], debug=0)


settings.receiver = None

Router.defaults(**settings.router_defaults)
Router.load(settings.handlers + settings.queue_handlers)
Router.RELOAD=False
Router.LOG_EXCEPTIONS=False

view.LOADER = jinja2.Environment(loader=jinja2.PackageLoader('lamson_tests', 'templates'))

# if you have pyenchant and enchant installed then the template tests will do
# spell checking for you, but you need to tell pyenchant where to find itself
if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
    os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson.png��������������������������������������������������������������������0000644�0000765�0000024�00000021310�11216071473�016204� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG

���
IHDR�����C���1���sBIT|d���	pHYs����~���tEXtCreation Time�5/17/09<@���tEXtSoftware�Adobe Fireworks CS4Ӡ�� �IDATx}yxEg2$3ȱ$ ŒdY9d]}A_,h7>&"d"ÍaI d~tuzp?SOTw]=]~j
4Aw4hLtww
uArԎ,\^Iyt~_[tT`
ipL4pA8Xpƻ\.*Sh$1 w& 7{y0H%hhID"Vx	7
&@QihH
jXy	C$hh7ID4O"A,՜P>(jE$hhHxJ,YS`uDh)GULJ$hhI?eM�DX2QDd&9g,xF"}rH2X㧚&<T`Vi�w[M 'G_SSbՠA"҆|ANY&$t|iUF2npKj$G4:e)Q;y<&ԑ&*
I"<agtXV:F'~I#{ԝDdT?vṥ`ViZΰ$ª?4xޭrl&@s xgJ>!@\͖wIn[ƞC%'}x`}?%$rd~,hZ"M@sH~14xoDH5~E ,lN C~q7U,G*yw笓@hI#jU7{7
i6.i@&@aQkw^=fC]KlAScfdS4�H�0Ui"W4+u|<TmFM
a%ʯ[3wSt	v\		Add$vɓ'#))	?OδaSTN5&7HaKn%m�Itp!`9DCoz;IγlwT&uY`f|K")<EfbXtܾO'4iҭ4tr
;yx7CŸ3&ʧh2Y߼9l6 PԈx4t}�-$N <Q6։˗']%t
8u?7oߎ8<E|	zk	{lӦ?Lz'.-<
PU~24I��	`.硖>q?#)&i\m\W~$<MK) 8m6云,VϟTg R?"RE-u$5/l%�DNՎ<$*C8Nmұc'={:uBRRbccg,YYYYŒ3Yfǖ-[?Xб!x
y~'<2{40C
k!FlHGV: fRm<Nұ/f4s>IIBsm/I6mյGoAi4$ug.�;`$MU%m {y.jeq&:AJy^dllF1G9|=<$_%k!iğxfE<6̣��ƌ3saɒ%ѣ1o<̛70!8˜3ϿE}a9 Zm-&rIx?Oҙ�Lʈ%h/#C\$7l#!CҎŐ^nrH+ȵ(G"x0RtdgvAȅLp۫2mI-u`cq'yqzu�'(c,l{I!HD}$4z)L@?>אfHJJ|^z	ӧOŋo_O?qa_?s"MCi_+HF<&H/DI^ԹNiw5%	w)0KL*mI#e̅;QY2,ܾ@2h)a�9#THLpo[{#" 1^,K)	 vJ`
Āj�30
[׺S*ͭOB[7w}:eϣiKbXf
&Mcɒ%Xt)‚u8+=ӗwCHS}OI	/	w.q<՜bN}T>9i�ʪQ&I8{
[W;0	GʕUŴBlTu�,*T{Ul-l6͖OԥI˛S$M!$4%	�¨6.J(sFNgD>8|&_�=z46n�8{%0m@
	tL^٘f\%uBH#|J<EFj:,;T*ތf^6:O@$,wI/<!;r^<1'Go6)ܼat6-[Wpl_=cJwIM[;t耠 wN
Ow;vL.]`݈�J4紃=w XFAA,ԹKP:>7N]nF[^
4Q_X^:]=[VQ#WhSnjDRlm#HXMeLdTtMEy왳0CVA[߿}=Z=DDFFӸvʋoU3	F(/{g	Id	H\>s?J9,	@H~&bZa]Wseߎ߯O\['fMJOO^�TTT`߾}X~=^}Ut	fB۶m\~�ѲeK�ۮy#+c/hBHPG@OkЄ3׏
T�DX"!24|a857Z޼~piO9(u"	©QS##kڬv=2e
3gD޽ѻwo\p(bر>}:~zy(W+\111:�ر[̚5E*S5H`2REQ;$)N@H쐌v(S(3$;K~]>A>!)^	ֈx44zҁ-ηE>h>w[3z^.fFkC׫yp՜;$ֵƏgGF6p:ظq#j*�@ll,fϞlL0ׯRRRK/sعs'ZnTGCNx*oɭ :F	I21B+03eDPպcWOպ3+DXF!�BWmDE>T7rH;vߟu&n`/_9|g/DӰj�:XO08tTUUlق+V˘?>V^+W"<<&	9v;BCC1hР:?^{
j֬nС=oڏzUާ$B"#xC|Ha:AboV#:7K:	D<ITF$"Y7?Yfrrr0|p#<<O=wֽb%oԷ1?Oh5!Ӧɓ'cĈnǙ3g0tP\("11GuS_w,L8ɓذqk|rtKcn#
e"aUmɻPȀ/OP$LȾYYRSϕFAK'B4lf/DԼUk?;nDŽ	x��a֯><_A?\t=ӽdӇ¿o}D0#77۶mÐ!Cc_FZZwcȑ#ZE�Я_?<l>,EUUV:v}{qG}%#ByC85CC!|(kK&R'1d*yZ8ʴ	Px׀Z""6I^⡚N{m6[ndܣ�	 Ճo_lc*Aw
ͅ	PLsrtF 2˱ylݺ${T#"7|5ɓ1x`(,,D^?	VB-zjXn]m9o6<N2<ƅ-O'U7uȲ`!b8!)"DxD’I]@-Hꥐ^
y:l2!u<uE>$<K'{&@"! S!sYCnzwN\-(qlTLྶE~7FCL@ӥzĀA3@D "{zzs9n?/<Gff&ۇVZᣏ>�l۶
=u_=8iӦ!--
3gĩSЫWM6 oڴ/idٲeXhN>VZ!HwjsL7\3ɛћBD&ҦDh(䑀&#	Tub}@~3B-e}9kC=kֆZ;>N$uϝ6G".+9sZ8z,o庡F?T̜9999ݻ7�`ҥX&?z|#--
Fw}˖-øq ŋ1"k֬V9qx
󥒊ʲfKϡ+v`5Hux	dɄVqĖ\Ѡ$r'[`@^W^yHII%@2n8VKϟĉq)^J"Xv-\|/wrRv3\ɘw?`C9ܷ?AdUvh0UFsiD#D4/xb&''#66HMMEAAR {Mo2лwo\�sٳt
EEEnˑ̙˷w,=kb]U =WOSDC!̎,"QF"߽mşY/;vΝ;k{cǎL?f|�U� !!'Nm69jOXǓn<?Xqf,Pf;8h!ӅEyϟ?o`Ʉu!99km|1m_teee^%$4k@۶ma0$-[%%%?>6,=kp``x[Ѡi$n`
<5tnd^^^P(--E\\>\ߪU+$$<͛7gQMѵkW޽;*++Ag*++QSSa#.:kYhNْk`ոDC`xJjB:6-g \^r:uژ1c+Wxͣ}ؽ{7QUUݜ.yjcMG\2(,wcdեVqyd)S^4%kBoJkʙϞ=y.1118}4nܸvjbڵX`U_霔={
.mS"Á6]F"yMPs,xj5XEj5(SŻxfZ h3M6$g6j5I"v$@{FϲBQQv͛7EǗ_~^xj3@W^EDDJKKX{`Ƿ5B[Bj_֘ʒڔaZ3 s=
DQG	eQwy5<5F&`7O*;Lά9s-o/^ڭ[7(**Ƹqc
W.\7nF~Ǐbqt"**
'N͛71j(�]p̯W1GI"9mImDEj0t|j)ݎx:@E'66M9a,ǫHg@P;<;o$"ɍD%�~:}ȇ}RNؼifGSг	C}
[8<nݧ&MBbbb111شiVXb޼x!ܹ3VX7x6l@PzBee%쯾VsfD])o-BWm-"[r]rVvCQy\ʍ�,@^</b.υiT%;]Q/EQ#jLb&rM&6	/o[ᵣڜMEQ4nNrVrKkZͤ?h
|)i02
D:06#C\:mkM[N|
fΜ#""_nXw}uiWJJ

=*9~x"z=>3̟?aaሎFAA
KGII	233qUGÓ8yNd<D VlZ]lPKy(fB)je"*@FA#yIAB:t19@"\]EIgjy`ۨ׎ll)V+On$*Wn(:5r-@&9V5IDQ4d@"a;	gX.K#U@T#~K϶7vz:⥋ҫe1u9xhױ|ؔ_I闚<ek:vz7((f}ń	mGk0n8TVI0v՗GMX^ԜV\ؕn<v@VVS~"!�V5Rϥ3"RL)K~-ΏI"4XEQx)nvPψH:k{$. Tm;aC*m3:xJ",hՠv=ծS8~97EAh_yYDo5>8(r}nOM<y2/_,"""eeeXx1
h۶
~{fϞ'O"==_l3Ý(2ޖ7ym)kgZi"#f.5GT.yf  IͪU$$**]wuFo}vvVix>Lr9tJ>=Nb}0^s?6&gS'zfXt)zꅇzݻŘ70qD2g믿BB=''/<
hҴ|G 50-]ؒkJ �#!<P.V5jxNP;?ܭ77J;nGeQ|\":ͅDFM
pWyd)D4/#mZ̵
�t:c`=Vy?&-ѰqA뗦8)0ի8vDm_M<;/^ê3݃^۔_p\lFH#H٢(:/A! g2D#p[}&H#o$uV&t.߮-f(F^,3$@n ykt9ji,v4P!%jϕ1ڽӡvFkNpT/NAq٤|>3<O'S#f4)Dn>7M9]Pz!UUz4Bl['~,Z%\4M72
]z8\.? $
sȄC]7.$fTb"qj߼vY_0zK<H b
2k'�4&摇F"z)ȐU.7{qa?xKPg)$G4}$@"Z%q,��IDAT_O"�\LhVDM,T\oFTIUq@5hG@HɓdUݡN"�K$6mf
û7zz|(yG&hp'\CD\Fj u|3B:>d^J
cSu/4h%6@v6;+%
)`	z[}HVRI"#%H@9?"aӪI"/L4a+u
@ �4FC$DIY,»yCݍHDC@`$~$K�ܫv+E3v`4 �hhDdK%:\-w/EMok-	hh;P$(䣯8ȃDxGMnVID44:p'l|M2F Zjtm$Jj$~Iw
LO_I<t;@24$ޑ=	~&/h$1@4hE
`c����IENDB`������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/�����������������������������������������������������������������0000755�0000765�0000024�00000000000�11313464575�016732� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/__init__.py������������������������������������������������������0000644�0000765�0000024�00000000026�11212223425�021022� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import config.testing
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/args_tests.py����������������������������������������������������0000644�0000765�0000024�00000013106�11241323533�021447� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
import os
from lamson import args, commands
from mock import *
import sys



def test_match():
    tokens = [["word", "test"],["int", 1]]
    assert args.match(tokens, "word") == "test", "Wrong word on match."
    assert args.match(tokens) == 1, "Wrong int on match."

    assert len(tokens) == 0, "There should be nothing in the array after matching."


@raises(args.ArgumentError)
def test_match_fails():
    tokens = [["word", "test"],["int", 1]]
    args.match(tokens, "string")



def test_peek():
    tokens = [["word", "test"],["int", 1]]
    assert args.peek(tokens, "word"), "There should be a word."
    assert len(tokens) == 2, "Args should not go down after peek."
    args.match(tokens, "word")

    assert args.peek(tokens, "int"), "There should be an int."
    assert not args.peek(tokens, "option"), "There should not be an option."
    args.match(tokens, "int")

@raises(args.ArgumentError)
def test_peek_fails():
    tokens = []
    args.peek(tokens, 'string')

def test_determine_kwargs():
    kw = args.determine_kwargs(commands.log_command)
    assert kw['pid']

def test_tokenize():
    tokens = args.tokenize(['test', '--num', '1', '--help', '--stuff', 'The remainder.'])
    assert args.match(tokens, 'word')
    assert args.match(tokens, 'option')
    assert args.match(tokens, 'int')
    assert args.match(tokens, 'option')
    assert args.match(tokens, 'option')
    assert args.match(tokens, 'string')

    # test a condition where there is a remainder that's not identified
    tokens = args.tokenize(['stop', '--pid', 'run/log.pid'])
    assert tokens
    assert args.match(tokens, 'word')
    assert args.match(tokens, 'option')
    assert args.match(tokens, 'string')


def test_parse():
    command, options = args.parse(['test', '--num', '1', '--help', '--stuff', 'The remainder.', '--tail'])
    assert command, "There should be a command."
    assert options, "There should be options."

    assert command == "test", "command should be test"
    assert options["num"] == 1, "num option wrong"
    assert options["help"] == True, "help should be true"
    assert options["stuff"] == 'The remainder.', "stuff should a string"
    assert options['tail'] == True, "There should be a True tail."

    command2, options = args.parse(['--num', '1', '--help', '--stuff', 'The remainder.'])
    assert not command2, "There should NOT be a command."
    assert options, "There should be options."
    assert options["num"] == 1, "num option wrong"
    assert options["help"] == True, "help should be true"
    assert options["stuff"] == 'The remainder.', "stuff should a string"


def test_defaults():
    command, options = args.parse(['test', '--num', '1', '--help', '--stuff', 'The remainder.'])
    args.ensure_defaults(options, {'num': 2, 'help': True, 'stuff': None})
    assert options['help'] == True
    assert options['num'] == 1
   
    command, options = args.parse(['test', '--num', '1', '--help', '--stuff', 'The remainder.'])
    args.ensure_defaults(options, {'num': 2, 'extras': 3, 'help': None, 
                                   'stuff': None})
    assert options['extras'] == 3
    assert options['num'] == 1


    assert_raises(args.ArgumentError,
                  args.ensure_defaults,
                  options, {'num': 2, 'extras': 3, 'help': None, 'TRAILING': None})

    assert_raises(args.ArgumentError,
                  args.ensure_defaults,
                  options, {'num': 2, 'extras': 3, 'help': None, 'bad': None})

def test_available_help():
    assert args.available_help(commands)    

def test_available_commands():
    assert args.available_commands(commands).index('help') >= 0, 'no help command'

@patch('sys.exit', new=Mock())
def test_parse_and_run_command():
    assert args.parse_and_run_command(['help'], commands,
                                      default_command=None)

    assert not args.parse_and_run_command(['badcommand'], commands,
                                      default_command=None, exit_on_error=False)
    assert not sys.exit.called

    assert not args.parse_and_run_command(['badcommand'], commands,
                                      default_command=None, exit_on_error=True)
    assert sys.exit.called

    assert not args.parse_and_run_command(['badcommand'], commands,
                                      default_command='help', exit_on_error=False)

    assert args.parse_and_run_command([], commands,
                                      default_command='send', exit_on_error=False)

    assert args.parse_and_run_command([], commands,
                                      default_command='help', exit_on_error=False)


def test_help_for_command():
    assert args.help_for_command(commands, "help")
    assert not args.help_for_command(commands, "badcommand")

def test_trailing():
    command, options = args.parse(['test', '--num', '1', '--', 'Trailing 1', 'Trailing 2'])
    expected = ['Trailing 1', 'Trailing 2']
    assert command == 'test'
    assert options['TRAILING']
    for e in expected: assert e in options['TRAILING']

    # test with a corner case of a switch option before trailing
    command, options = args.parse(['test', '--num', '1', '--switch', '--', 'Trailing 1', 'Trailing 2'])
    for e in expected: assert e in options['TRAILING']


@patch('sys.exit', new=Mock())
def test_invalid_options():
    args.parse_and_run_command(['log -foobar'], commands)

    args.parse_and_run_command(['log badarg'], commands)
    assert sys.exit.called

def test_no_command_or_default():
    args.parse_and_run_command([], commands,
                                      default_command=None,
                                      exit_on_error=False)

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/bounce_filtered_mod.py�������������������������������������������0000644�0000765�0000024�00000001326�11226470171�023266� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson.routing import route, route_like
from lamson.bounce import bounce_to


SOFT_RAN=False
HARD_RAN=False

@route(".+")
def SOFT_BOUNCED(message):
    global SOFT_RAN
    SOFT_RAN=True
    # remember to transition back to START or the mailer daemon 
    # at that host will be put in a bad state
    return START

@route(".+")
def HARD_BOUNCED(message):
    global HARD_RAN
    HARD_RAN=True
    # remember to transition back to START or the mailer daemon 
    # at that host will be put in a bad state
    return START

@route("(anything)@(host)", anything=".+", host=".+")
@bounce_to(soft=SOFT_BOUNCED, hard=HARD_BOUNCED)
def START(message, **kw):
    return END

@route_like(START)
def END(message, *kw):
    pass


����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/bounce_tests.py��������������������������������������������������0000644�0000765�0000024�00000006527�11241324203�021772� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson import mail
from lamson.routing import Router


def test_bounce_analyzer_on_bounce():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    assert bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 1.0
    assert bm.bounce.probable()
    assert_equal(bm.bounce.primary_status, (5, u'Permanent Failure'))
    assert_equal(bm.bounce.secondary_status, (1, u'Addressing Status'))
    assert_equal(bm.bounce.combined_status, (11, u'Bad destination mailbox address'))

    assert bm.bounce.is_hard()
    assert_equal(bm.bounce.is_hard(), not bm.bounce.is_soft())

    assert_equal(bm.bounce.remote_mta, u'gmail-smtp-in.l.google.com')
    assert_equal(bm.bounce.reporting_mta, u'mail.zedshaw.com')
    assert_equal(bm.bounce.final_recipient,
                 u'asdfasdfasdfasdfasdfasdfewrqertrtyrthsfgdfgadfqeadvxzvz@gmail.com')
    assert_equal(bm.bounce.diagnostic_codes[0], u'550-5.1.1')
    assert_equal(bm.bounce.action, 'failed')
    assert 'Content-Description-Parts' in bm.bounce.headers

    assert bm.bounce.error_for_humans()

def test_bounce_analyzer_on_regular():
    bm = mail.MailRequest(None,None,None, open("tests/signed.msg").read())
    assert not bm.is_bounce()
    assert bm.bounce
    assert bm.bounce.score == 0.0
    assert not bm.bounce.probable()
    assert_equal(bm.bounce.primary_status, (None, None))
    assert_equal(bm.bounce.secondary_status, (None, None))
    assert_equal(bm.bounce.combined_status, (None, None))

    assert not bm.bounce.is_hard()
    assert not bm.bounce.is_soft()

    assert_equal(bm.bounce.remote_mta, None)
    assert_equal(bm.bounce.reporting_mta, None)
    assert_equal(bm.bounce.final_recipient, None)
    assert_equal(bm.bounce.diagnostic_codes, [None, None])
    assert_equal(bm.bounce.action, None)


def test_bounce_to_decorator():
    import bounce_filtered_mod
    msg = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())

    Router.deliver(msg)
    assert Router.in_state(bounce_filtered_mod.START, msg)
    assert bounce_filtered_mod.HARD_RAN, "Hard bounce state didn't actually run: %r" % msg.route_to

    msg.bounce.primary_status = (4, u'Persistent Transient Failure')
    Router.clear_states()
    Router.deliver(msg)
    assert Router.in_state(bounce_filtered_mod.START, msg)
    assert bounce_filtered_mod.SOFT_RAN, "Soft bounce didn't actually run."

    msg = mail.MailRequest(None, None, None, open("tests/signed.msg").read())
    Router.clear_states()
    Router.deliver(msg)
    assert Router.in_state(bounce_filtered_mod.END, msg), "Regular messages aren't delivering."


def test_bounce_getting_original():
    msg = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    msg.is_bounce()

    assert msg.bounce.notification
    assert msg.bounce.notification.body

    assert msg.bounce.report

    for part in msg.bounce.report:
        assert [(k,part[k]) for k in part]
        # these are usually empty, but might not be.  they are in our test
        assert not part.body

    assert msg.bounce.original
    assert_equal(msg.bounce.original['to'], msg.bounce.final_recipient)
    assert msg.bounce.original.body


def test_bounce_no_headers_error_message():
    msg = mail.MailRequest(None, None, None, "Nothing")
    msg.is_bounce()
    assert_equal(msg.bounce.error_for_humans(), 'No status codes found in bounce message.')

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/command_tests.py�������������������������������������������������0000644�0000765�0000024�00000014757�11251311763�022151� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import commands, utils, mail, routing, encoding
from lamson.testing import spelling
from nose.tools import *
import os
import shutil
from mock import *
import sys
import imp


def setup():
    if os.path.exists("run/fake.pid"):
        os.unlink("run/fake.pid")

def teardown():
    if os.path.exists("run/fake.pid"):
        os.unlink("run/fake.pid")

def make_fake_pid_file():
    f = open("run/fake.pid","w")
    f.write("0")
    f.close()


def test_send_command():
    commands.send_command(sender='test@localhost',
                           to='test@localhost',
                           body='Test body',
                           subject='Test subject',
                           attach='setup.py',
                           port=8899,debug=0)

def test_status_command():
    commands.status_command(pid='run/log.pid')
    commands.status_command(pid='run/donotexist.pid')


@patch('sys.exit', new=Mock())
def test_help_command():
    commands.help_command()
    commands.help_command(**{'for': 'status'})

    # test with an invalid command
    commands.help_command(**{'for': 'invalid_command'})
    assert sys.exit.called

@patch('lamson.queue.Queue')
@patch('sys.exit', new=Mock())
def test_queue_command(MockQueue):
    mq = MockQueue()
    mq.get.return_value = "A sample message"
    mq.keys.return_value = ["key1","key2"]
    mq.pop.return_value = ('key1', 'message1')
    mq.count.return_value = 1
    
    commands.queue_command(pop=True)
    assert mq.pop.called
    
    commands.queue_command(get='somekey')
    assert mq.get.called
    
    commands.queue_command(remove='somekey')
    assert mq.remove.called
    
    commands.queue_command(clear=True)
    assert mq.clear.called
    
    commands.queue_command(keys=True)
    assert mq.keys.called

    commands.queue_command(count=True)
    assert mq.count.called

    commands.queue_command()
    assert sys.exit.called


@patch('sys.exit', new=Mock())
def test_gen_command():
    project = 'tests/testproject'
    if os.path.exists(project):
        shutil.rmtree(project)

    commands.gen_command(project=project)
    assert os.path.exists(project)

    # test that it exits if the project exists
    commands.gen_command(project=project)
    assert sys.exit.called

    sys.exit.reset_mock()
    commands.gen_command(project=project, FORCE=True)
    assert not sys.exit.called

    shutil.rmtree(project)


def test_routes_command():
    commands.routes_command(TRAILING=['lamson.handlers.log',
                                      'lamson.handlers.queue'])

    # test with the -test option
    commands.routes_command(TRAILING=['lamson.handlers.log',
                                      'lamson.handlers.queue'],
                            test="anything@localhost")

    # test with the -test option but no matches
    routing.Router.clear_routes()
    commands.routes_command(TRAILING=[], test="anything@localhost")


@patch('sys.exit', new=Mock())
@patch('lamson.utils.daemonize', new=Mock())
@patch('lamson.server.SMTPReceiver')
def test_log_command(MockSMTPReceiver):
    ms = MockSMTPReceiver()
    ms.start.function()

    setup()  # make sure it's clear for fake.pid
    commands.log_command(pid="run/fake.pid")
    assert utils.daemonize.called
    assert ms.start.called

    # test that it exits on existing pid
    make_fake_pid_file()
    commands.log_command(pid="run/fake.pid")
    assert sys.exit.called

@patch('sys.stdin', new=Mock())
def test_sendmail_command():
    sys.stdin.read.function()

    msg = mail.MailResponse(To="tests@localhost", From="tests@localhost",
                            Subject="Hello", Body="Test body.")
    sys.stdin.read.return_value = str(msg)
    commands.sendmail_command(port=8899)

@patch('sys.exit', new=Mock())
@patch('lamson.utils.daemonize', new=Mock())
@patch('lamson.utils.import_settings', new=Mock())
@patch('lamson.utils.drop_priv', new=Mock())
@patch('sys.path', new=Mock())
def test_start_command():
    # normal start
    commands.start_command()
    assert utils.daemonize.called
    assert utils.import_settings.called

    # start with pid file existing already
    make_fake_pid_file()
    commands.start_command(pid="run/fake.pid")
    assert sys.exit.called

    # start with pid file existing and force given
    assert os.path.exists("run/fake.pid")
    commands.start_command(FORCE=True, pid="run/fake.pid")
    assert not os.path.exists("run/fake.pid")

    # start with a uid but no gid
    commands.start_command(uid=1000, gid=False, pid="run/fake.pid", FORCE=True)
    assert not utils.drop_priv.called

    # start with a uid/gid given that's valid
    commands.start_command(uid=1000, gid=1000, pid="run/fake.pid", FORCE=True)
    assert utils.drop_priv.called



def raise_OSError(*x, **kw):
    raise OSError('Fail')

@patch('sys.exit', new=Mock())
@patch('os.kill', new=Mock())
@patch('glob.glob', new=lambda x: ['run/fake.pid'])
def test_stop_command():
    # gave a bad pid file
    try:
        commands.stop_command(pid="run/dontexit.pid")
    except IOError:
        assert sys.exit.called

    make_fake_pid_file()
    commands.stop_command(pid="run/fake.pid")

    make_fake_pid_file()
    commands.stop_command(ALL="run")

    make_fake_pid_file()
    commands.stop_command(pid="run/fake.pid", KILL=True)
    assert os.kill.called
    assert not os.path.exists("run/fake.pid")

    make_fake_pid_file()
    os.kill.side_effect = raise_OSError
    commands.stop_command(pid="run/fake.pid", KILL=True)


@patch('glob.glob', new=lambda x: ['run/fake.pid'])
@patch('lamson.utils.daemonize', new=Mock())
@patch('lamson.utils.import_settings', new=Mock())
@patch('os.kill', new=Mock())
@patch('sys.exit', new=Mock())
@patch('sys.path', new=Mock())
def test_restart_command():
    make_fake_pid_file()
    commands.restart_command(pid="run/fake.pid")

@patch('os.chdir', new=Mock())
@patch('BaseHTTPServer.HTTPServer', new=Mock())
@patch('SimpleHTTPServer.SimpleHTTPRequestHandler', new=Mock())
def test_web_command():
    commands.web_command()
    assert os.chdir.called

def test_version_command():
    commands.version_command()


def test_cleanse_command():
    commands.cleanse_command(input='run/queue', output='run/cleansed')
    assert os.path.exists('run/cleansed')

def raises_EncodingError(*args):
    raise encoding.EncodingError

@patch('lamson.encoding.from_message')
def test_cleans_command_with_encoding_error(from_message):
    from_message.side_effect = raises_EncodingError
    commands.cleanse_command(input='run/queue', output='run/cleansed')


def test_blast_command():
    commands.blast_command(input='run/queue', port=8899)

�����������������lamson-1.0pre11/tests/lamson_tests/confirm_tests.py�������������������������������������������������0000644�0000765�0000024�00000005061�11251754772�022167� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.confirm import *
from lamson.testing import *
from lamson import mail, queue
import shutil
import os


def teardown():
    if os.path.exists('run/confirm'):
        shutil.rmtree('run/confirm')

    if os.path.exists('run/queue'):
        shutil.rmtree('run/queue')


teardown()
storage = ConfirmationStorage()
engine = ConfirmationEngine('run/confirm', storage)


def test_ConfirmationStorage():
    storage.store('testing', 'somedude@localhost',
                  '12345', '567890')
    secret, pending_id = storage.get('testing', 'somedude@localhost')
    assert_equal(secret, '12345')
    assert_equal(pending_id, '567890')

    storage.delete('testing', 'somedude@localhost')
    assert_equal(len(storage.confirmations), 0)

    storage.store('testing', 'somedude@localhost',
                  '12345', '567890')
    assert_equal(len(storage.confirmations), 1)
    storage.clear()
    assert_equal(len(storage.confirmations), 0)


def test_ConfirmationEngine_send():
    queue.Queue('run/queue').clear()
    engine.clear()

    list_name = 'testing'
    action = 'subscribing to'
    host = 'localhost'

    message = mail.MailRequest('fakepeer', 'somedude@localhost',
                               'testing-subscribe@localhost', 'Fake body.')

    engine.send(relay(port=8899), 'testing', message, 'confirmation.msg', locals())
   
    confirm = delivered('confirm')
    assert delivered('somedude', to_queue=engine.pending)
    assert confirm

    return confirm

def test_ConfirmationEngine_verify():
    confirm = test_ConfirmationEngine_send()

    resp = mail.MailRequest('fakepeer', '"Somedude Smith" <somedude@localhost>',
                           confirm['Reply-To'], 'Fake body')

    target, _, expect_secret = confirm['Reply-To'].split('-')
    expect_secret = expect_secret.split('@')[0]

    found = engine.verify(target, resp['from'], 'invalid_secret')
    assert not found

    pending = engine.verify(target, resp['from'], expect_secret)
    assert pending, "Verify failed: %r not in %r." % (expect_secret,
                                                      storage.confirmations)

    assert_equal(pending['from'], 'somedude@localhost')
    assert_equal(pending['to'], 'testing-subscribe@localhost')


def test_ConfirmationEngine_cancel():
    confirm = test_ConfirmationEngine_send()

    target, _, expect_secret = confirm['Reply-To'].split('-')
    expect_secret = expect_secret.split('@')[0]

    engine.cancel(target, confirm['To'], expect_secret)
    
    found = engine.verify(target, confirm['To'], expect_secret)
    assert not found
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/encoding_tests.py������������������������������������������������0000644�0000765�0000024�00000023527�11257715313�022321� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import with_statement
from nose.tools import *
import re
import os
from lamson import encoding, mail
import mailbox
import email
from email import encoders
from email.utils import parseaddr
from mock import *
import chardet


BAD_HEADERS = [
    u'"\u8003\u53d6\u5206\u4eab" <Ernest.Beard@msa.hinet.net>'.encode('utf-8'),
    '"=?windows-1251?B?RXhxdWlzaXRlIFJlcGxpY2E=?="\n\t<wolfem@barnagreatlakes.com>',
    '=?iso-2022-jp?B?Zmlicm91c19mYXZvcmF0ZUB5YWhvby5jby5qcA==?=<fibrous_favorate@yahoo.co.jp>',

    '=?windows-1252?Q?Global_Leadership_in_HandCare_-_Consumer,\n\t_Professional_and_Industrial_Products_OTC_:_FLKI?=',
    '=?windows-1252?q?Global_Leadership_in_Handcare_-_Consumer, _Auto,\n\t_Professional_&_Industrial_Products_-_OTC_:_FLKI?=',
    'I am just normal.',
    '=?koi8-r?B?WW91ciBtYW6ScyBzdGFtaW5hIHdpbGwgY29tZSBiYWNrIHRvIHlvdSBs?=\n\t=?koi8-r?B?aWtlIGEgYm9vbWVyYW5nLg==?=',
    '=?koi8-r?B?WW91IGNhbiBiZSBvbiB0b3AgaW4gYmVkcm9vbSBhZ2FpbiCWIGp1c3Qg?=\n\t=?koi8-r?B?YXNrIHVzIGZvciBhZHZpY2Uu?=',
    '"=?koi8-r?B?5MXMz9DSz8na18/E09TXzw==?=" <daniel@specelec.com>',
    '=?utf-8?b?IumrlOiCsuWckuWNgOermSDihpIg6ZW35bqa6Yar6Zmi56uZIOKGkiDmlofljJbk?=\n =?utf-8?b?uInot6/nq5kiIDx2Z3hkcmp5Y2lAZG5zLmh0Lm5ldC50dz4=?=',
    '=?iso-8859-1?B?SOlhdnkgTel05WwgVW7uY/hk?=\n\t=?iso-8859-1?Q?=E9?=',
]

DECODED_HEADERS = encoding.header_from_mime_encoding(BAD_HEADERS)

NORMALIZED_HEADERS = [encoding.header_to_mime_encoding(x) for x in DECODED_HEADERS]


def test_MailBase():
    the_subject = u'p\xf6stal'
    m = encoding.MailBase()
    
    m['To'] = "testing@localhost"
    m['Subject'] = the_subject

    assert m['To'] == "testing@localhost"
    assert m['TO'] == m['To']
    assert m['to'] == m['To']

    assert m['Subject'] == the_subject
    assert m['subject'] == m['Subject']
    assert m['sUbjeCt'] == m['Subject']
    
    msg = encoding.to_message(m)
    m2 = encoding.from_message(msg)

    assert_equal(len(m), len(m2))

    for k in m:
        assert m[k] == m2[k], "%s: %r != %r" % (k, m[k], m2[k])
    
    for k in m.keys():
        assert k in m
        del m[k]
        assert not k in m

def test_header_to_mime_encoding():
    for i, header in enumerate(DECODED_HEADERS):
        assert_equal(NORMALIZED_HEADERS[i], encoding.header_to_mime_encoding(header))

def test_dumb_shit():
    # this is a sample of possibly the worst case Mutt can produce
    idiot = '=?iso-8859-1?B?SOlhdnkgTel05WwgVW7uY/hk?=\n\t=?iso-8859-1?Q?=E9?='
    should_be = u'H\xe9avy M\xe9t\xe5l Un\xeec\xf8d\xe9'
    assert_equal(encoding.header_from_mime_encoding(idiot), should_be)

def test_header_from_mime_encoding():
    assert not encoding.header_from_mime_encoding(None)
    assert_equal(len(BAD_HEADERS), len(encoding.header_from_mime_encoding(BAD_HEADERS)))
    
    for i, header in enumerate(BAD_HEADERS):
        assert_equal(DECODED_HEADERS[i], encoding.header_from_mime_encoding(header))


def test_to_message_from_message_with_spam():
    mb = mailbox.mbox("tests/spam")
    fails = 0
    total = 0

    for msg in mb:
        try:
            m = encoding.from_message(msg)
            out = encoding.to_message(m)
            assert repr(out)

            m2 = encoding.from_message(out)

            for k in m:
                if '@' in m[k]:
                    assert_equal(parseaddr(m[k]), parseaddr(m2[k]))
                else:
                    assert m[k].strip() == m2[k].strip(), "%s: %r != %r" % (k, m[k], m2[k])

                assert not m[k].startswith(u"=?")
                assert not m2[k].startswith(u"=?")
                assert m.body == m2.body, "Bodies don't match" 

                assert_equal(len(m.parts), len(m2.parts), "Not the same number of parts.")

                for i, part in enumerate(m.parts):
                    assert part.body == m2.parts[i].body, "Part %d isn't the same: %r \nvs\n. %r" % (i, part.body, m2.parts[i].body)
            total += 1
        except encoding.EncodingError, exc:
            fails += 1

    assert fails/total < 0.01, "There were %d failures out of %d total." % (fails, total)


def test_to_file_from_file():
    mb = mailbox.mbox("tests/spam")
    msg = encoding.from_message(mb[0])

    outfile = "run/encoding_test.msg"

    with open(outfile, 'w') as outfp:
        encoding.to_file(msg, outfp)

    with open(outfile) as outfp:
        msg2 = encoding.from_file(outfp)
    
    outdata = open(outfile).read()

    assert_equal(len(msg), len(msg2))
    os.unlink(outfile)


def test_guess_encoding_and_decode():
    for header in DECODED_HEADERS:
        try:
            encoding.guess_encoding_and_decode('ascii', header.encode('utf-8'))
        except encoding.EncodingError:
            pass


def test_attempt_decoding():
    for header in DECODED_HEADERS:
        encoding.attempt_decoding('ascii', header.encode('utf-8'))


def test_properly_decode_header():
    for i, header in enumerate(BAD_HEADERS):
        parsed = encoding.properly_decode_header(header)
        assert_equal(DECODED_HEADERS[i], parsed)


def test_headers_round_trip():
    # round trip the headers to make sure they convert reliably back and forth
    for header in BAD_HEADERS:
        original = encoding.header_from_mime_encoding(header)

        assert original
        assert "=?" not in original and "?=" not in original, "Didn't decode: %r" % (encoding.SCANNER.scan(header),)

        encoded = encoding.header_to_mime_encoding(original)
        assert encoded

        return_original = encoding.header_from_mime_encoding(encoded)
        assert_equal(original, return_original)

        return_encoded = encoding.header_to_mime_encoding(return_original)
        assert_equal(encoded, return_encoded)


def test_MIMEPart():
    text1 = encoding.MIMEPart("text/plain")
    text1.set_payload("The first payload.")
    text2 = encoding.MIMEPart("text/plain")
    text2.set_payload("The second payload.")

    image_data = open("tests/lamson.png").read()
    img1 = encoding.MIMEPart("image/png")
    img1.set_payload(image_data)
    img1.set_param('attachment','', header='Content-Disposition')
    img1.set_param('filename','lamson.png', header='Content-Disposition')
    encoders.encode_base64(img1)
    
    multi = encoding.MIMEPart("multipart/mixed")
    for x in [text1, text2, img1]:
        multi.attach(x)

    mail = encoding.from_message(multi)

    assert mail.parts[0].body == "The first payload."
    assert mail.parts[1].body == "The second payload."
    assert mail.parts[2].body == image_data

    encoding.to_message(mail)


@patch('chardet.detect', new=Mock())
@raises(encoding.EncodingError)
def test_guess_encoding_fails_completely():
    chardet.detect.return_value = {'encoding': None, 'confidence': 0.0}
    encoding.guess_encoding_and_decode('ascii', 'some data', errors='strict')


def test_attach_text():
    mail = encoding.MailBase()
    mail.attach_text("This is some text.", 'text/plain')

    msg = encoding.to_message(mail)
    assert msg.get_payload(0).get_payload() == "This is some text."
    assert encoding.to_string(mail)

    mail.attach_text("<html><body><p>Hi there.</p></body></html>", "text/html")
    msg = encoding.to_message(mail)
    assert len(msg.get_payload()) == 2
    assert encoding.to_string(mail)


def test_attach_file():
    mail = encoding.MailBase()
    png = open("tests/lamson.png").read()
    mail.attach_file("lamson.png", png, "image/png", "attachment")
    msg = encoding.to_message(mail)

    payload = msg.get_payload(0)
    assert payload.get_payload(decode=True) == png
    assert payload.get_filename() == "lamson.png", payload.get_filename()



def test_content_encoding_headers_are_maintained():
    inmail = encoding.from_file(open("tests/signed.msg"))

    ctype, ctype_params = inmail.content_encoding['Content-Type']

    assert_equal(ctype, 'multipart/signed')

    # these have to be maintained
    for key in ['protocol', 'micalg']:
        assert key in ctype_params

    # these get removed
    for key in encoding.CONTENT_ENCODING_REMOVED_PARAMS:
        assert key not in ctype_params

    outmsg = encoding.to_message(inmail)
    ctype, ctype_params = encoding.parse_parameter_header(outmsg, 'Content-Type')
    for key in ['protocol', 'micalg']:
        assert key in ctype_params, key


def test_odd_content_type_with_charset():
    mail = encoding.MailBase()
    mail.body = u"p\xf6stal".encode('utf-8')
    mail.content_encoding['Content-Type'] = ('application/plain', {'charset': 'utf-8'})

    msg = encoding.to_string(mail)
    assert msg

def test_specially_borked_lua_message():
    assert encoding.from_file(open("tests/borked.msg"))

def raises_TypeError(*args):
    raise TypeError()

@patch('lamson.encoding.MIMEPart.__init__')
@raises(encoding.EncodingError)
def test_to_message_encoding_error(mp_init):
    mp_init.side_effect = raises_TypeError
    test = encoding.from_file(open("tests/borked.msg"))
    msg = encoding.to_message(test)

def raises_UnicodeError(*args):
    raise UnicodeError()

@raises(encoding.EncodingError)
def test_guess_encoding_and_decode_unicode_error():
    data = Mock()
    data.__str__ = Mock()
    data.__str__.return_value = u"\0\0"
    data.decode.side_effect = raises_UnicodeError
    encoding.guess_encoding_and_decode("ascii", data)
    
def test_attempt_decoding_with_bad_encoding_name():
    assert_equal("test", encoding.attempt_decoding("asdfasdf", "test"))

@raises(encoding.EncodingError)
def test_apply_charset_to_header_with_bad_encoding_char():
    encoding.apply_charset_to_header('ascii', 'X', 'bad')

def test_odd_roundtrip_bug():
    decoded_addrs=[u'"\u0414\u0435\u043b\u043e\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u043e" <daniel@specelec.com>',
                   u'"\u8003\u53d6\u5206\u4eab" <Ernest.Beard@msa.hinet.net>',
                   u'"Exquisite Replica"\n\t<wolfem@barnagreatlakes.com>',]

    for decoded in decoded_addrs:
        encoded = encoding.header_to_mime_encoding(decoded)
        assert '<' in encoded and '"' in encoded, "Address wasn't encoded correctly:\n%s" % encoded

�������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/handler_tests.py�������������������������������������������������0000644�0000765�0000024�00000000472�11206570377�022145� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.routing import Router
from lamson_tests import message_tests
import lamson.handlers.log
import lamson.handlers.queue

def test_log_handler():
    Router.deliver(message_tests.test_mail_request())

def test_queue_handler():
    Router.deliver(message_tests.test_mail_request())
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/html_tests.py����������������������������������������������������0000644�0000765�0000024�00000005042�11251311317�021455� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from config import testing
from lamson import html, view, mail



def test_HtmlMail_load_css():
    title = "load_css Test"
    hs = html.HtmlMail("style.css", "html_test.html")
    assert_equal(len(hs.stylesheet), 8)
    assert_equal(hs.stylesheet[0][0], u"body")


def test_HtmlMail_apply_styles():
    hs = html.HtmlMail("style.css", "html_test.html")
    page = view.render(locals(), "html_test.html")

    styled = hs.apply_styles(page)

    assert "magenta" in str(styled)
    assert_not_equal(str(styled), str(page))


def test_HtmlMail_render():
    title = "render Test"
    hs = html.HtmlMail("style.css", "html_test.html")

    lame = hs.render(locals(), "content.markdown")
    assert lame

    pretty = hs.render(locals(), "content.markdown", pretty=True)

    assert pretty
    assert_not_equal(lame, pretty)


def test_HtmlMail_respond():
    title = "respond Test"
    hs = html.HtmlMail("style.css", "html_test.html")
    variables = locals()

    msg = hs.respond(variables, "content.markdown", From='somedude@localhost',
                     To='zed.shaw@gmail.com',
                     Subject='This is a %(title)s')

    assert 'content' not in variables
    assert msg
    assert_equal(msg['from'], 'somedude@localhost')
    assert_equal(msg['to'], 'zed.shaw@gmail.com')
    assert_equal(msg['subject'], "This is a respond Test")


def test_HtmlMail_content_type_respected():
    generator = html.HtmlMail("style.css", "html_test.html", {})

    resp = generator.respond({}, "content.markdown",
                           From="somedude@localhost",
                           To="somedude@localhost",
                           Subject="Test of an HTML mail.",
                           Body="content.markdown"
                           )

    req = mail.MailRequest('fakepeer', None, None, str(resp))
    
    assert_equal(req.base.content_encoding['Content-Type'][0], 'multipart/alternative')

    resp2 = mail.MailResponse(To=req['to'],
                              From=req['from'],
                              Subject=req['subject'])
    resp2.attach_all_parts(req)
    
    assert_equal(resp2.base.content_encoding['Content-Type'],
                 resp.base.content_encoding['Content-Type'])
    
    assert_equal(resp2.base.content_encoding['Content-Type'][0], 'multipart/alternative')

    req2 = mail.MailRequest('fakepeer', None, None, str(resp2))

    assert_equal(resp2.base.content_encoding['Content-Type'][0], 'multipart/alternative')

    assert_equal(req2.base.content_encoding['Content-Type'][0], 'multipart/alternative')

����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/message_tests.py�������������������������������������������������0000644�0000765�0000024�00000021035�11257715412�022147� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (C) 2008 Zed A. Shaw.  Licensed under the terms of the GPLv3.

import warnings
from nose.tools import *
import re
import os
from lamson import mail, encoding
import email

sample_message = """From: somedude@localhost
To: somedude@localhost

Test
"""

def test_mail_request():
    # try with a half-assed message
    msg = mail.MailRequest("localhost", "zedfrom@localhost",
                           "zedto@localhost", "Fake body.")
    assert msg['to'] == "zedto@localhost", "To is %r" % msg['to']
    assert msg['from'] == "zedfrom@localhost", "From is %r" % msg['from']

    msg = mail.MailRequest("localhost", "somedude@localhost",
                             ["somedude@localhost"], sample_message)
    assert msg.original == sample_message

    assert_equal(msg['From'], "somedude@localhost")

    assert("From" in msg)
    del msg["From"]
    assert("From" not in msg)

    msg["From"] = "nobody@localhost"
    assert("From" in msg)
    assert_equal(msg["From"], "nobody@localhost")

    # validate that upper and lower case work for headers
    assert("FroM" in msg)
    assert("from" in msg)
    assert("From" in msg)
    assert_equal(msg['From'], msg['fRom'])
    assert_equal(msg['From'], msg['from'])
    assert_equal(msg['from'], msg['fRom'])

    # make sure repr runs
    print repr(msg)

    return msg

def test_mail_response_plain_text():
    sample = mail.MailResponse(To="receiver@localhost", 
                                 Subject="Test message",
                                 From="sender@localhost",
                                 Body="Test from test_mail_response_plain_text.")
    return sample

def test_mail_response_html():
    sample = mail.MailResponse(To="receiver@localhost", 
                                 Subject="Test message",
                                 From="sender@localhost",
                                 Html="<html><body><p>From test_mail_response_html</p></body></html>")
    return sample

def test_mail_response_html_and_plain_text():
    sample = mail.MailResponse(To="receiver@localhost", 
                                 Subject="Test message",
                                 From="sender@localhost",
                                 Html="<html><body><p>Hi there.</p></body></html>",
                                 Body="Test from test_mail_response_html_and_plain_text.")
    return sample

def test_mail_response_attachments():
    sample = mail.MailResponse(To="receiver@localhost", 
                                 Subject="Test message",
                                 From="sender@localhost",
                                 Body="Test from test_mail_response_attachments.")
    readme_data = open("./README").read()

    assert_raises(AssertionError, sample.attach, filename="./README", disposition="inline")

    sample.attach(filename="./README", content_type="text/plain", disposition="inline")
    assert len(sample.attachments) == 1
    assert sample.multipart

    msg = sample.to_message()
    assert_equal(len(msg.get_payload()), 2)

    sample.clear()
    assert len(sample.attachments) == 0
    assert not sample.multipart

    sample.attach(data=readme_data, filename="./README", content_type="text/plain")

    msg = sample.to_message()
    assert_equal(len(msg.get_payload()), 2)
    sample.clear()

    sample.attach(data=readme_data, content_type="text/plain")
    msg = sample.to_message()
    assert_equal(len(msg.get_payload()), 2)

    return sample


def test_mail_request_attachments():
    sample = test_mail_response_attachments()
    data = str(sample)

    msg = mail.MailRequest("localhost", None, None, data)

    msg_parts = msg.all_parts()
    sample_parts = sample.all_parts()

    readme = open("./README").read()

    # BUG: Python's MIME text attachment decoding drops trailing newline chars

    assert msg_parts[0].body == sample_parts[0].body
    # python drops chars at the end, so can't compare equally
    assert readme.startswith(msg_parts[1].body)
    assert msg.body() == sample_parts[0].body

    # test that we get at least one message for messages without attachments
    sample = test_mail_response_plain_text()
    msg = mail.MailRequest("localhost", None, None, str(sample))
    msg_parts = msg.all_parts()
    assert len(msg_parts) == 0, "Length is %d should be 0." % len(msg_parts)
    assert msg.body()


def test_mail_response_mailing_list_headers():
    list_addr = "test.users@localhost"

    msg = mail.MailResponse(From='somedude@localhost', To=list_addr, 
            Subject='subject', Body="Mailing list reply.")

    print repr(msg)

    msg["Sender"] = list_addr
    msg["Reply-To"] = list_addr
    msg["List-Id"] = list_addr
    msg["Return-Path"] = list_addr
    msg["In-Reply-To"] = 'Message-Id-1231123'
    msg["References"] = 'Message-Id-838845854'
    msg["Precedence"] = 'list'

    data = str(msg)

    req = mail.MailRequest('localhost', 'somedude@localhost', list_addr, data)

    headers = ['Sender', 'Reply-To', 'List-Id', 'Return-Path', 
               'In-Reply-To', 'References', 'Precedence']
    for header in headers:
        assert msg[header] == req[header]

    # try a delete
    del msg['Precedence']

def test_mail_response_ignore_case_headers():
    msg = test_mail_response_plain_text()
    # validate that upper and lower case work for headers
    assert("FroM" in msg)
    assert("from" in msg)
    assert("From" in msg)
    assert_equal(msg['From'], msg['fRom'])
    assert_equal(msg['From'], msg['from'])
    assert_equal(msg['from'], msg['fRom'])


def test_walk():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    parts = [x for x in bm.walk()]

    assert parts
    assert_equal(len(parts), 6)


def test_copy_parts():
    bm = mail.MailRequest(None,None,None, open("tests/bounce.msg").read())
    
    resp = mail.MailResponse(To=bm['to'], From=bm['from'],
                             Subject=bm['subject'])

    resp.attach_all_parts(bm)

    resp = resp.to_message()
    bm = bm.to_message()

    assert_equal(len([x for x in bm.walk()]), len([x for x in resp.walk()]))

   
def test_craft_from_sample():
    list_name = "test.list"
    user_full_address = "tester@localhost"

    sample = mail.MailResponse(To=list_name + "@localhost",
                          From=user_full_address,
                          Subject="Test message with attachments.",
                          Body="The body as one attachment.")
    sample.update({"Test": "update"})

    sample.attach(filename="tests/lamson_tests/message_tests.py",
                  content_type="text/plain",
                  disposition="attachment")
    
    inmsg = mail.MailRequest("fakepeer", None, None, str(sample))
    assert "Test" in sample.keys()

    for part in inmsg.to_message().walk():
        assert part.get_payload(), "inmsg busted."

    outmsg = mail.MailResponse(To=inmsg['from'], 
                          From=inmsg['to'],
                          Subject=inmsg['subject'])

    outmsg.attach_all_parts(inmsg)

    result = outmsg.to_message()

    for part in result.walk():
        assert part.get_payload(), "outmsg parts don't have payload."


def test_route_to_from_works():
    msg = mail.MailRequest("fakepeer", "from@localhost",
                                   [u"<to1@localhost>", u"to2@localhost"], "")
    assert '<' not in msg.route_to, msg.route_to

    msg = mail.MailRequest("fakepeer", "from@localhost",
                                   [u"to1@localhost", u"to2@localhost"], "")
    assert '<' not in msg.route_to, msg.route_to
    
    msg = mail.MailRequest("fakepeer", "from@localhost",
                                   [u"to1@localhost", u"<to2@localhost>"], "")
    assert '<' not in msg.route_to, msg.route_to

    msg = mail.MailRequest("fakepeer", "from@localhost",
                                   [u"to1@localhost"], "")
    assert '<' not in msg.route_to, msg.route_to

    msg = mail.MailRequest("fakepeer", "from@localhost",
                                   [u"<to1@localhost>"], "")
    assert '<' not in msg.route_to, msg.route_to


def test_decode_header_randomness():
    assert_equal(mail._decode_header_randomness(None), set())
    assert_equal(mail._decode_header_randomness(["z@localhost", '"Z A" <z@localhost>']), 
                 set(["z@localhost", "z@localhost"]))
    assert_equal(mail._decode_header_randomness("z@localhost"),
                 set(["z@localhost"]))
    assert_raises(encoding.EncodingError, mail._decode_header_randomness, 1)


def test_msg_is_deprecated():
    warnings.simplefilter("ignore")
    msg = mail.MailRequest(None, None, None, "")
    assert_equal(msg.msg, msg.base)
    resp = mail.MailResponse()
    assert_equal(resp.msg, resp.base)

���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/queue_tests.py���������������������������������������������������0000644�0000765�0000024�00000006757�11251311625�021655� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import queue, server, mail
from nose.tools import *
import shutil
import os
from mock import *
import mailbox

USE_SAFE=False

def setup():
    if os.path.exists("run/big_queue"):
        shutil.rmtree("run/big_queue")

def teardown():
    setup()


def test_push():
    q = queue.Queue("run/queue", safe=USE_SAFE)
    q.clear()

    # the queue doesn't really care if its a request or response, as long
    # as the object answers to str(msg)
    msg = mail.MailResponse(To="test@localhost", From="test@localhost",
                              Subject="Test", Body="Test")
    key = q.push(msg)
    assert key, "Didn't get a key for test_get push."

    return q


def test_pop():
    q = test_push()
    key, msg = q.pop()

    assert key, "Didn't get a key for test_get push."
    assert msg, "Didn't get a message for key %r" % key

    assert msg['to'] == "test@localhost"
    assert msg['from'] == "test@localhost"
    assert msg['subject'] == "Test"
    assert msg.body() == "Test"

    assert q.count() == 0, "Queue should be empty."
    assert not q.pop()[0]


def test_get():
    q = test_push()
    msg = mail.MailResponse(To="test@localhost", From="test@localhost",
                              Subject="Test", Body="Test")
    key = q.push(str(msg))
    assert key, "Didn't get a key for test_get push."

    msg = q.get(key)
    assert msg, "Didn't get a message for key %r" % key

def test_remove():
    q = test_push()
    msg = mail.MailResponse(To="test@localhost", From="test@localhost",
                              Subject="Test", Body="Test")
    key = q.push(str(msg))
    assert key, "Didn't get a key for test_get push."
    assert q.count() == 2, "Wrong count %d should be 2" % q.count()

    q.remove(key)
    assert q.count() == 1, "Wrong count %d should be 1" % q.count()



def test_safe_maildir():
    global USE_SAFE
    USE_SAFE=True
    test_push()
    test_pop()
    test_get()
    test_remove()


def test_oversize_protections():
    # first just make an oversize limited queue
    overq = queue.Queue("run/queue", pop_limit=10)
    overq.clear()

    for i in range(5):
        overq.push("HELLO" * 100)

    assert_equal(overq.count(), 5)

    key, msg = overq.pop()

    assert not key and not msg, "Should get no messages."
    assert_equal(overq.count(), 0)

    # now make sure that oversize mail is moved to the overq
    setup()
    overq = queue.Queue("run/queue", pop_limit=10, oversize_dir="run/big_queue")
    moveq = queue.Queue("run/big_queue")

    for i in range(5):
        overq.push("HELLO" * 100)

    key, msg = overq.pop()

    assert not key and not msg, "Should get no messages."
    assert_equal(overq.count(), 0)
    assert_equal(moveq.count(), 5)

    moveq.clear()
    overq.clear()


@patch('os.stat', new=Mock())
@raises(mailbox.ExternalClashError)
def test_SafeMaildir_name_clash():
    try:
        shutil.rmtree("run/queue")
    except: pass
    sq = queue.SafeMaildir('run/queue')
    sq.add("TEST")

def raise_OSError(*x, **kw):
    err = OSError('Fail')
    err.errno = 0
    raise err

@patch('mailbox._create_carefully', new=Mock())
@raises(OSError)
def test_SafeMaildir_throws_errno_failure():
    setup()
    mailbox._create_carefully.side_effect = raise_OSError
    sq = queue.SafeMaildir('run/queue')
    sq.add("TEST")

@patch('os.stat', new=Mock())
@raises(OSError)
def test_SafeMaildir_reraise_weird_errno():
    try:
        shutil.rmtree("run/queue")
    except: pass

    os.stat.side_effect = raise_OSError
    sq = queue.SafeMaildir('run/queue')
    sq.add('TEST')

�����������������lamson-1.0pre11/tests/lamson_tests/routing_tests.py�������������������������������������������������0000644�0000765�0000024�00000011022�11251312351�022172� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson.routing import *
from lamson.mail import MailRequest
from lamson import queue, routing, encoding
from mock import *


def setup():
    Router.clear_states()
    Router.clear_routes()

def teardown():
    setup()

def test_MemoryStorage():
    store = MemoryStorage()
    store.set(test_MemoryStorage.__module__, "tester@localhost", "TESTED")

    assert store.get(test_MemoryStorage.__module__, "tester@localhost") == "TESTED"

    assert store.get(test_MemoryStorage.__module__, "tester2@localhost") == "START"

    store.clear()

    assert store.get(test_MemoryStorage.__module__, "tester@localhost") == "START"

def test_ShelveStorage():
    store = ShelveStorage("tests/statesdb")

    store.set(test_ShelveStorage.__module__, "tester@localhost", "TESTED")
    assert store.get(test_MemoryStorage.__module__, "tester@localhost") == "TESTED"

    assert store.get(test_MemoryStorage.__module__, "tester2@localhost") == "START"

    store.clear()
    assert store.get(test_MemoryStorage.__module__, "tester@localhost") == "START"


def test_RoutingBase():
    assert len(Router.ORDER) == 0
    assert len(Router.REGISTERED) == 0

    Router.load(['lamson_tests.simple_fsm_mod'])
    import simple_fsm_mod

    assert len(Router.ORDER) > 0
    assert len(Router.REGISTERED) > 0

    message = MailRequest('fakepeer', 'zedshaw@localhost', 'users-subscribe@localhost', "")
    Router.deliver(message)
    assert Router.in_state(simple_fsm_mod.CONFIRM, message)

    confirm = MailRequest('fakepeer', '"Zed Shaw" <zedshaw@localhost>',  'users-confirm-1@localhost', "")
    Router.deliver(confirm)
    assert Router.in_state(simple_fsm_mod.POSTING, message)

    Router.deliver(message)
    assert Router.in_state(simple_fsm_mod.NEXT, message)

    Router.deliver(message)
    assert Router.in_state(simple_fsm_mod.END, message)

    Router.deliver(message)
    assert Router.in_state(simple_fsm_mod.START, message)

    Router.clear_states()
    explosion = MailRequest('fakepeer',  '<hacker@localhost>',   'start-explode@localhost', "")
    Router.LOG_EXCEPTIONS=True
    Router.deliver(explosion)

    assert Router.in_error(simple_fsm_mod.END, explosion)

    Router.clear_states()
    Router.LOG_EXCEPTIONS=False
    explosion = MailRequest('fakepeer',  'hacker@localhost',   'start-explode@localhost', "")
    assert_raises(RuntimeError, Router.deliver, explosion)

    Router.reload()
    assert 'lamson_tests.simple_fsm_mod' in Router.HANDLERS
    assert len(Router.ORDER)
    assert len(Router.REGISTERED)


    message = MailRequest('fakepeer', 'zedshaw@localhost',
                          ['users-subscribe@localhost',
                           'users-confirm-1@localhost'], "Fake body.")

    Router.deliver(message)

    assert Router.in_state(simple_fsm_mod.POSTING, message), "Router state: %r" % Router.get_state('simple_fsm_mod', message)


def test_Router_undeliverable_queue():
    Router.clear_routes()
    Router.clear_states()

    Router.UNDELIVERABLE_QUEUE = Mock()
    msg = MailRequest('fakepeer', 'from@localhost', 'to@localhost', "Nothing")

    Router.deliver(msg)
    assert Router.UNDELIVERABLE_QUEUE.push.called



@raises(NotImplementedError)
def test_StateStorage_get_raises():
    s = StateStorage()
    s.get("raises", "raises")

@raises(NotImplementedError)
def test_StateStorage_set_raises():
    s = StateStorage()
    s.set("raises", "raises", "raises")

@raises(NotImplementedError)
def test_StateStorage_clear_raises():
    s = StateStorage()
    s.clear()

@raises(TypeError)
def test_route___get___raises():
    class BadRoute(object):

        @route("test")
        def wont_work(message, **kw):
            pass

    br = BadRoute()
    br.wont_work("raises")

def raises_ImportError(*args):
    raise ImportError()

@patch('__builtin__.reload', new=Mock())
@patch('__builtin__.__import__', new=Mock())
@patch('lamson.routing.LOG', new=Mock())
def test_reload_raises():
    reload.side_effect = raises_ImportError
    __import__.side_effect = raises_ImportError

    Router.LOG_EXCEPTIONS=True
    Router.reload()
    assert routing.LOG.exception.called

    Router.LOG_EXCEPTIONS=False
    routing.LOG.exception.reset_mock()
    assert_raises(ImportError, Router.reload)
    assert not routing.LOG.exception.called

    routing.LOG.exception.reset_mock()
    Router.LOG_EXCEPTIONS=True
    Router.load(['fake.handler'])
    assert routing.LOG.exception.called

    Router.LOG_EXCEPTIONS=False
    routing.LOG.exception.reset_mock()
    assert_raises(ImportError, Router.load, ['fake.handler'])
    assert not routing.LOG.exception.called


��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/server_tests.py��������������������������������������������������0000644�0000765�0000024�00000007110�11251312351�022014� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (C) 2008 Zed A. Shaw.  Licensed under the terms of the GPLv3.

from nose.tools import *
from mock import *
import os
from lamson import server, queue, routing
from message_tests import *
import re


def test_router():
    routing.Router.deliver(test_mail_request())

    # test that fallthrough works too
    msg = test_mail_request()
    msg['to'] = 'unhandled@localhost'
    msg.To = msg['to']

    routing.Router.deliver(msg)

def test_receiver():
    receiver = server.SMTPReceiver(host="localhost", port=8824)
    msg = test_mail_request()
    receiver.process_message(msg.Peer, msg.From, msg.To, str(msg))


def test_relay_deliver():
    relay = server.Relay("localhost", port=8899)

    relay.deliver(test_mail_response_plain_text())
    relay.deliver(test_mail_response_html())
    relay.deliver(test_mail_response_html_and_plain_text())
    relay.deliver(test_mail_response_attachments())

@patch('DNS.mxlookup')
def test_relay_deliver_mx_hosts(DNS_mxlookup):
    DNS_mxlookup.return_value = [[100, "localhost"]]
    relay = server.Relay(None, port=8899)

    msg = test_mail_response_plain_text()
    msg['to'] = 'zedshaw@localhost'
    relay.deliver(msg)
    assert DNS_mxlookup.called

@patch('DNS.mxlookup')
def test_relay_resolve_relay_host(DNS_mxlookup):
    DNS_mxlookup.return_value = []
    relay = server.Relay(None, port=8899)
    host = relay.resolve_relay_host('zedshaw@localhost')
    assert_equal(host, 'localhost')
    assert DNS_mxlookup.called

    DNS_mxlookup.reset_mock()
    DNS_mxlookup.return_value = [[100, "mail.zedshaw.com"]]
    host = relay.resolve_relay_host('zedshaw@zedshaw.com')
    assert_equal(host, 'mail.zedshaw.com')
    assert DNS_mxlookup.called

def test_relay_reply():
    relay = server.Relay("localhost", port=8899)
    print "Relay: %r" % relay

    relay.reply(test_mail_request(), 'from@localhost', 'Test subject', 'Body')

def raises_exception(*x, **kw):
    raise RuntimeError("Raised on purpose.")


@patch('lamson.routing.Router', new=Mock())
def test_queue_receiver():
    receiver = server.QueueReceiver('run/queue')
    run_queue = queue.Queue('run/queue')
    run_queue.push(str(test_mail_response_plain_text()))
    assert run_queue.count() > 0
    receiver.start(one_shot=True)
    assert run_queue.count() == 0

    routing.Router.deliver.side_effect = raises_exception
    receiver.process_message(mail.MailRequest('localhost', 'test@localhost',
                                              'test@localhost', 'Fake body.'))



@patch('threading.Thread', new=Mock())
@patch('lamson.routing.Router', new=Mock())
def test_SMTPReceiver():
    receiver = server.SMTPReceiver(port=9999)
    receiver.start()
    receiver.process_message('localhost', 'test@localhost', 'test@localhost',
                             'Fake body.')

    routing.Router.deliver.side_effect = raises_exception
    receiver.process_message('localhost', 'test@localhost', 'test@localhost',
                             'Fake body.')

    receiver.close()

def test_SMTPError():
    err = server.SMTPError(550)
    assert str(err) == '550 Permanent Failure Mail Delivery Protocol Status', "Error is wrong: %r" % str(err)

    err = server.SMTPError(400)
    assert str(err) == '400 Persistent Transient Failure Other or Undefined Status', "Error is wrong: %r" % str(err)

    err = server.SMTPError(425)
    assert str(err) == '425 Persistent Transient Failure Mailbox Status', "Error is wrong: %r" % str(err)

    err = server.SMTPError(999)
    assert str(err) == "999 ", "Error is wrong: %r" % str(err)

    err = server.SMTPError(999, "Bogus Error Code")
    assert str(err) == "999 Bogus Error Code"

��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/simple_fsm_mod.py������������������������������������������������0000644�0000765�0000024�00000003021�11251311637�022264� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson.routing import *

@state_key_generator
def simple_key_gen(module_name, message):
    return module_name

# common routing capture regexes go in here, you can override them in @route
Router.defaults(host="localhost", 
                action="[a-zA-Z0-9]+",
                list_name="[a-zA-Z.0-9]+")


@route("(list_name)-(action)@(host)")
def START(message, list_name=None, action=None, host=None):
    print "START", message, list_name, action, host
    if action == 'explode':
        print "EXPLODE!"
        raise RuntimeError("Exploded on purpose.")
    return CONFIRM
    
@route("(list_name)-confirm-(id_number)@(host)", id_number="[0-9]+")
def CONFIRM(message, list_name=None, id_number=None, host=None):
    print "CONFIRM", message, list_name, id_number, host
    return POSTING

@route("(list_name)-(action)@(host)")
def POSTING(message, list_name=None, action=None, host=None):
    print "POSTING", message, list_name, action, host
    return NEXT

@route_like(POSTING)
def NEXT(message, list_name=None, action=None, host=None):
    print "NEXT", message, list_name, action, host
    return END

@route("(anything)@(host)", anything=".*")
def END(message, anything=None, host=None):
    print "END", anything, host
    return START

@route(".*")
@stateless
@nolocking
def PASSING(message, *args, **kw):
    print "PASSING", args, kw


try:
    @stateless
    @route("badstateless@(host)")
    def BAD_STATELESS(message, *args, **kw):
        print "BAD_STATELESS", args, kw
except AssertionError:
    pass  # we need to get this
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/spam_filtered_mod.py���������������������������������������������0000644�0000765�0000024�00000000713�11234142500�022741� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson.routing import route, route_like
from lamson.spam import spam_filter


ham_db = "tests/sddb"

@route(".+")
def SPAMMING(message):
    # the spam black hole
    pass

@route("(anything)@(host)", anything=".+", host=".+")
@spam_filter(ham_db, "tests/.hammierc", "run/queue", next_state=SPAMMING)
def START(message, **kw):
    print "Ham message received. Going to END."
    return END

@route_like(START)
def END(message, **kw):
    print "Done."


�����������������������������������������������������lamson-1.0pre11/tests/lamson_tests/spam_tests.py����������������������������������������������������0000644�0000765�0000024�00000002465�11242110562�021456� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson import spam
from lamson_tests.message_tests import *
from lamson.routing import Router
import os

ham_db = "tests/sddb"

def setup():
    Router.clear_states()
    Router.clear_routes()
    if os.path.exists(ham_db):
        os.unlink(ham_db)

def teardown():
    setup()

def test_Filter():
    sf = spam.Filter(ham_db, 'tests/.hammierc')
    ham_msg = test_mail_request().to_message()
    spam_msg = test_mail_response_plain_text().to_message()

    sf.train_ham(ham_msg)
    sf.train_spam(spam_msg)

    sf.untrain_ham(test_mail_request().to_message())
    sf.untrain_spam(spam_msg)


def test_spam_filter():
    import spam_filtered_mod

    sf = spam.Filter(ham_db, 'tests/.hammierc')
    msg = test_mail_request()
    sf.train_spam(msg.to_message())

    Router.deliver(msg)
    assert Router.in_state(spam_filtered_mod.SPAMMING, msg), "Spam got through"

    Router.clear_states()
    sf.untrain_spam(msg.to_message())
    sf.train_ham(msg.to_message())

    Router.deliver(msg)
    assert Router.in_state(spam_filtered_mod.END, msg), "Ham didn't go through."

    del spam_filtered_mod


def test_spam_filter_without_db_file():
    import spam_filtered_mod

    msg = test_mail_request()
    Router.deliver(msg)
    assert Router.in_state(spam_filtered_mod.END, msg), "Spam got through"

�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/�������������������������������������������������������0000755�0000765�0000024�00000000000�11313464575�020730� 5����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/confirmation.msg���������������������������������������0000644�0000765�0000024�00000001070�11242120756�024115� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hi,

You (or someone pretending to be you) has asked to {{action}}
{{list_name}}, but before I can do that I need you to confirm that you
really want to do this.  I know, I know, you hate having to confirm
everything you do, but if I don't confirm your request, then someone who
hates you can forge your identity.

We wouldn't want that now would we?  Good!


CONFIRM BY REPLYING

If you really did mean {{ list_name }}, then reply to this message
and I'll do it right away.  I'll send you one more message telling you
when I'm done.

Thank You,

      librelist.com

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/content.markdown���������������������������������������0000644�0000765�0000024�00000000217�11234723727�024145� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hello
=====


I would *love* for you to tell me what is going on here joe.  NOW!


Alright
-------


This is the best I can come up with.

Zed
���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/html_test.html�����������������������������������������0000644�0000765�0000024�00000000346�11241675465�023626� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<html>
    <head>
        <title>{{ title }}</title>
    </head>

    <body style="background: magenta">
        <h1 class="bright">{{ title }}</h1>

        {{ content }}

        <h3 id="dull">All done.</h3>
    </body>
</html>
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/style.css����������������������������������������������0000644�0000765�0000024�00000000563�11235000074�022565� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������body:
    margin: 10
    padding: 20
    background: green - 30
    color: blue

    h1:
        font-size: 3em
    h2:
        font-size: 2em
        color: yellow

    h3:
        font-size: 1em

    p:
        padding: 0.3em
        background: red

h2:
    color: yellow

#bright:
    background: black
    color: white

.dull:
    background: gray
    color: black

���������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/template.html������������������������������������������0000644�0000765�0000024�00000000134�11220207113�023403� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<html>
    <body>
        <p>Hey there {{dude}} I like your shirt.</p>
    </body>
</html>

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/template.txt�������������������������������������������0000644�0000765�0000024�00000000047�11220204460�023263� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Hey there {{dude}} I like your shirt.

�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/templates/unicode.html�������������������������������������������0000644�0000765�0000024�00000000706�11221057547�023242� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
    <title>Iñtërnâtiônàlizætiøn</title>
</head>

<body>
    <h2>{{dude}} Iñtërnâtiônàlizætiøn</h2>
    <p>Τὸ πλοῖόν μου τὸ μετεωριζόμενον ἐστι<br />
πλῆρες ἐγχελέων.</p>
    </body>
</html>

����������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/testing_tests.py�������������������������������������������������0000644�0000765�0000024�00000002747�11251311667�022207� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from lamson import server
from lamson.routing import Router
from lamson.testing import *
from nose.tools import *
import os

relay = relay(port=8899)

def setup():
    Router.clear_routes()
    Router.clear_states()
    Router.load(['lamson_tests.simple_fsm_mod'])


def test_clear_queue():
    queue().push("Test")
    assert queue().count() > 0

    clear_queue()
    assert queue().count() == 0


def test_relay():
    clear_queue()
    relay.send('test@localhost', 'zedshaw@localhost', 'Test message', 'Test body')
    assert queue().keys()

def test_delivered():
    clear_queue()
    relay.send("zedshaw@localhost", "tester@localhost", Subject="Test subject.", Body="Test body.")
    assert delivered("zedshaw@localhost"), "Test message not delivered."
    assert delivered("zedshaw@localhost"), "Test message not delivered."
    assert not delivered("badman@localhost")
    assert_in_state('lamson_tests.simple_fsm_mod', 'zedshaw@localhost', 'tester@localhost', 'START')

def test_RouterConversation():
    client = RouterConversation('tester@localhost', 'Test router conversations.')
    client.begin()
    client.say('testlist@localhost', 'This is a test')

def test_spelling():
    # specific to a mac setup, because macs are lame
    if 'PYENCHANT_LIBRARY_PATH' not in os.environ:
        os.environ['PYENCHANT_LIBRARY_PATH'] = '/opt/local/lib/libenchant.dylib'

    template = "tests/lamson_tests/templates/template.txt"
    contents = open(template).read()
    assert spelling(template, contents) 
�������������������������lamson-1.0pre11/tests/lamson_tests/utils_tests.py���������������������������������������������������0000644�0000765�0000024�00000002135�11241721461�021655� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson import utils, view
from mock import *


def test_make_fake_settings():
    settings = utils.make_fake_settings('localhost', 8800)
    assert settings
    assert settings.receiver
    assert settings.relay == None
    settings.receiver.close()

def test_import_settings():
    loader = view.LOADER

    settings = utils.import_settings(True, from_dir='tests', boot_module='config.testing')
    assert settings
    assert settings.receiver_config

    view.LOADER = loader
    settings = utils.import_settings(False, from_dir='examples/osb')
    assert settings
    assert settings.receiver_config



@patch('daemon.DaemonContext.open')
def test_daemonize_not_fully(dc_open):
    context = utils.daemonize("run/tests.pid", ".", False, False, do_open=False)
    assert context
    assert not dc_open.called
    dc_open.reset_mock()

    context = utils.daemonize("run/tests.pid", ".", "/tmp", 0002, do_open=True)
    assert context
    assert dc_open.called


@patch("daemon.daemon.change_process_owner")
def test_drop_priv(cpo):
    utils.drop_priv(100, 100)
    assert cpo.called

�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/lamson_tests/view_tests.py����������������������������������������������������0000644�0000765�0000024�00000005222�11251311646�021470� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������from nose.tools import *
from lamson import view
import jinja2


def test_load():
    template = view.load("template.txt")
    assert template
    assert template.render()

def test_render():
    # try with some empty vars
    text = view.render({}, "template.txt")
    assert text


def test_most_basic_form():
    msg = view.respond(locals(), 'template.txt')
    assert msg.Body

def test_respond_cadillac_version():
    dude = 'Tester'

    msg = view.respond(locals(), Body='template.txt', 
                      Html='template.html',
                      From='test@localhost',
                      To='receiver@localhost',
                      Subject='Test body from "%(dude)s".')

    assert msg.Body
    assert msg.Html

    for k in ['From', 'To', 'Subject']:
        assert k in msg


def test_respond_plain_text():
    dude = 'Tester'

    msg = view.respond(locals(), Body='template.txt', 
                      From='test@localhost',
                      To='receiver@localhost',
                      Subject='Test body from "%(dude)s".')

    assert msg.Body
    assert not msg.Html

    for k in ['From', 'To', 'Subject']:
        assert k in msg



def test_respond_html_only():
    dude = 'Tester'

    msg = view.respond(locals(), Html='template.html', 
                      From='test@localhost',
                      To='receiver@localhost',
                      Subject='Test body from "%(dude)s".')

    assert not msg.Body
    assert msg.Html

    for k in ['From', 'To', 'Subject']:
        assert k in msg



def test_respond_attach():
    dude = "hello"
    mail = view.respond(locals(), Body="template.txt",
                       From="test@localhost",
                       To="receiver@localhost",
                       Subject='Test body from someone.')

    view.attach(mail, locals(), 'template.html', content_type="text/html",
               filename="template.html", disposition='attachment')

    assert_equal(len(mail.attachments), 1)

    msg = mail.to_message()
    assert_equal(len(msg.get_payload()), 2)
    assert str(msg)

    mail.clear()

    view.attach(mail, locals(), 'template.html', content_type="text/html")
    assert_equal(len(mail.attachments), 1)

    msg = mail.to_message()
    assert_equal(len(msg.get_payload()), 2)
    assert str(msg)


def test_unicode():
    dude = u'H\xe9avy M\xe9t\xe5l Un\xeec\xf8d\xe9'
    mail = view.respond(locals(), Html="unicode.html",
                       From="test@localhost",
                       To="receiver@localhost",
                       Subject='Test body from someone.')
    assert str(mail)

    view.attach(mail, locals(), "unicode.html", filename="attached.html")

    assert str(mail)

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/signed.msg��������������������������������������������������������������������0000644�0000765�0000024�00000005720�11250551014�016165� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������From: sender@example.dom
To: recipient@example.dom
Subject: M2Crypto S/MIME testing
MIME-Version: 1.0
Content-Type: MULTIPART/SIGNED; protocol="application/x-pkcs7-signature"; micalg=sha1; boundary="----E0F28B7E67DFE4661A697A8B50B85ACE"

This is an S/MIME signed message

------E0F28B7E67DFE4661A697A8B50B85ACE
Content-Type: application/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64

YSBww7ZzdGFsIHNpZ24gb2Ygb3VyIHRpbWVz
------E0F28B7E67DFE4661A697A8B50B85ACE
Content-Type: application/x-pkcs7-signature; name="smime.p7s"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7s"

MIIGuQYJKoZIhvcNAQcCoIIGqjCCBqYCAQExCzAJBgUrDgMCGgUAMIGfBgkqhkiG
9w0BBwGggZEEgY5Db250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL3BsYWluOyBjaGFy
c2V0PSJ1dGYtOCINCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVRyYW5zZmVy
LUVuY29kaW5nOiBiYXNlNjQNCg0KWVNCd3c3WnpkR0ZzSUhOcFoyNGdiMllnYjNW
eUlIUnBiV1Z6oIID3jCCA9owggNDoAMCAQICCQCbjddTwoA3QjANBgkqhkiG9w0B
AQUFADCBpTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQH
EwhOZXcgWW9yazEjMCEGA1UEChMaTGFtc29uIE1haWwgU2VydmVyIFByb2plY3Qx
ETAPBgNVBAsTCFJlbGVhc2VzMRQwEgYDVQQDEwtaZWQgQS4gU2hhdzEiMCAGCSqG
SIb3DQEJARYTemVkc2hhd0B6ZWRzaGF3LmNvbTAeFw0wOTA2MjAxODI4NDVaFw0x
MDA2MjAxODI4NDVaMIGlMQswCQYDVQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsx
ETAPBgNVBAcTCE5ldyBZb3JrMSMwIQYDVQQKExpMYW1zb24gTWFpbCBTZXJ2ZXIg
UHJvamVjdDERMA8GA1UECxMIUmVsZWFzZXMxFDASBgNVBAMTC1plZCBBLiBTaGF3
MSIwIAYJKoZIhvcNAQkBFhN6ZWRzaGF3QHplZHNoYXcuY29tMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDLmYSS/AqmPrgJP276q6eXrGKnpWW9CUrrLnQaPi0v
YKRJwROvYn2KoFvKUERB+AM3uPIFiHFtov6krXT0oRFEskmNPACkcIwj+royyR7i
gD9Y6y/HOb+JwaZHHGHxbylWwdb5Y0jFWEWhp6Y79/2BhEq9dbIyAlA/stiGf7ma
YwIDAQABo4IBDjCCAQowHQYDVR0OBBYEFCjGKUDQ4uoUDptWpKuuaXvIXcsuMIHa
BgNVHSMEgdIwgc+AFCjGKUDQ4uoUDptWpKuuaXvIXcsuoYGrpIGoMIGlMQswCQYD
VQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMSMw
IQYDVQQKExpMYW1zb24gTWFpbCBTZXJ2ZXIgUHJvamVjdDERMA8GA1UECxMIUmVs
ZWFzZXMxFDASBgNVBAMTC1plZCBBLiBTaGF3MSIwIAYJKoZIhvcNAQkBFhN6ZWRz
aGF3QHplZHNoYXcuY29tggkAm43XU8KAN0IwDAYDVR0TBAUwAwEB/zANBgkqhkiG
9w0BAQUFAAOBgQBhmkhKDLxHgpkuGSVFMoJEEZFnGxwTMncgSuQnuxJg2IZlf9qX
fMBuUfL/lHSJvc4brulqjsZ1JEZ+tGfqAEbQeNTha/deow4eoAy8rXl2CTcb+pHO
dNvbJ7k2xKFhEe35UeKKKhGrtkozGH+F6V2puKsJJbcTrYhd4Wx3zZh4yTGCAg4w
ggIKAgEBMIGzMIGlMQswCQYDVQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAP
BgNVBAcTCE5ldyBZb3JrMSMwIQYDVQQKExpMYW1zb24gTWFpbCBTZXJ2ZXIgUHJv
amVjdDERMA8GA1UECxMIUmVsZWFzZXMxFDASBgNVBAMTC1plZCBBLiBTaGF3MSIw
IAYJKoZIhvcNAQkBFhN6ZWRzaGF3QHplZHNoYXcuY29tAgkAm43XU8KAN0IwCQYF
Kw4DAhoFAKCBsTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
BTEPFw0wOTA2MjIwMzIyNTNaMCMGCSqGSIb3DQEJBDEWBBR0a/as2onYtck20lbK
QQCQWNN0ojBSBgkqhkiG9w0BCQ8xRTBDMAoGCCqGSIb3DQMHMA4GCCqGSIb3DQMC
AgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIBKDANBgkq
hkiG9w0BAQEFAASBgLi15b9XbFFMTWZmfEs1aZibjKCbZ9Ji4hgX5fGRBFT02UYk
8cF7CbSGNnYLqRsHY8F28oRou0HlTJFA7V0hgL7k+Qmd0fpcoqNMQndZVsPw0cbo
sH74X5YhhANkC00+EL2zU5lVvGFDveDp+/IsboxEYi1+bqousY3sjP6e2yj7

------E0F28B7E67DFE4661A697A8B50B85ACE--


������������������������������������������������lamson-1.0pre11/tests/spam��������������������������������������������������������������������������0000644�0000765�0000024�00000276120�11220017410�015065� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������From MAILER-DAEMON Sun May 24 09:51:04 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs275635fao;
	Mon, 18 May 2009 06:43:55 -0700 (PDT)
Received: by 10.90.100.11 with SMTP id x11mr5907104agb.85.1242654233978;
	Mon, 18 May 2009 06:43:53 -0700 (PDT)
Return-Path: <larrybythesea@sbcglobal.net>
Received: from localhost ([221.128.125.136])
	by mx.google.com with SMTP id 3si7557347aga.5.2009.05.18.06.43.51;
	Mon, 18 May 2009 06:43:53 -0700 (PDT)
Received-SPF: neutral (google.com: 221.128.125.136 is neither permitted nor
	denied by domain of larrybythesea@sbcglobal.net)
	client-ip=221.128.125.136; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 221.128.125.136 is neither
	permitted nor denied by domain of larrybythesea@sbcglobal.net)
	smtp.mail=larrybythesea@sbcglobal.net
X-Mailer: QMail
X-Priority: 3 (Normal)
To: <anonymous@anonymous.com>
Date: Mon, 18 May 2009 19:50:49 +0300
Message-ID: <01C9D7B7.00BC73B5@localhost>
Subject: Small love party tonight
Reply-To: <larrybythesea@sbcglobal.net>
From: <larrybythesea@sbcglobal.net>
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 7bit
X-Spambayes-Trained: spam
Status: O
Content-Length: 97
Lines: 6

http://www.netcom-brzesko.pl/1.html

bbnaiek  otarb
bdpk ueobd  gkrea
tittu kbk  bg a iub


From MAILER-DAEMON Sun May 24 09:51:10 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs392423fao;
	Tue, 19 May 2009 08:29:50 -0700 (PDT)
Received: by 10.115.89.18 with SMTP id r18mr350503wal.111.1242746988852;
	Tue, 19 May 2009 08:29:48 -0700 (PDT)
Return-Path: <sandraolivagey@flyclored.eti.br>
Received: from flyclored.eti.br (flyclored43.flyclored.eti.br [187.16.204.43])
	by mx.google.com with SMTP id k14si41002waf.60.2009.05.19.08.29.46; 
	Tue, 19 May 2009 08:29:47 -0700 (PDT)
Received-SPF: pass (google.com: domain of sandraolivagey@flyclored.eti.br
	designates 187.16.204.43 as permitted sender)
	client-ip=187.16.204.43; 
DomainKey-Status: good (test mode)
Authentication-Results: mx.google.com; spf=pass (google.com: domain of
	sandraolivagey@flyclored.eti.br designates 187.16.204.43 as
	permitted sender) smtp.mail=sandraolivagey@flyclored.eti.br;
	domainkeys=pass (test mode) header.From=sandraolivagey@flyclored.eti.br
DomainKey-Signature: a=rsa-sha1; q=dns; c=simple; s=dns; d=flyclored.eti.br;
	h=Message-ID:Date:From:User-Agent:MIME-Version:To:Subject:Content-Type:Content-Transfer-Encoding;
	b=QIPC5REObxkVcJD3L9k/CiavSzuHeLRl9GiUR+L7kksWnxsscL17yzL1+zGX82j/xs2JY3DF1FUN2fF+BnlCjw==;
Message-ID: <A295FDC7.14C0DB08@flyclored.eti.br>
Date: Tue, 19 May 2009 09:03:52 -0700
From: "Delfa Bigonsiali" <sandraolivagey@flyclored.eti.br>
User-Agent: Mozilla/5.0 (Windows; U; Win95; en-GB;
	rv:0.9.4) Gecko/20011019 Netscape6/6.2
MIME-Version: 1.0
To: <anonymous@anonymous.com>
Subject: Veja no seu pc
Content-Type: text/plain;
	charset="us-ascii"
Content-Transfer-Encoding: 8bit
X-Spambayes-Trained: spam
Status: O
Content-Length: 333
Lines: 12

Veja canais de TV no seu PC - Super Novidade!
================================
http://www.flyclored.eti.br/t
================================
PROGRAMAO VIVO. Nova tecnologia que utiliza a banda da sua Internet para
conectar canais de TV 

Veja canais de TV enquanto navega pela Internet

Sem mensalidades e anuidades



From MAILER-DAEMON Sun May 24 09:48:44 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs2853fao;
	Mon, 20 Apr 2009 10:10:42 -0700 (PDT)
Received: by 10.100.202.8 with SMTP id z8mr2233685anf.74.1240247437857;
	Mon, 20 Apr 2009 10:10:37 -0700 (PDT)
Return-Path: <irisryan@ms39.hinet.net>
Received: from 209.85.133.27 ([87.248.128.134])
	by mx.google.com with SMTP id b37si8077486ana.32.2009.04.20.10.09.27;
	Mon, 20 Apr 2009 10:10:37 -0700 (PDT)
Received-SPF: neutral (google.com: 87.248.128.134 is neither permitted nor
	denied by best guess record for domain of
	irisryan@ms39.hinet.net) client-ip=87.248.128.134; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 87.248.128.134 is neither
	permitted nor denied by best guess record for domain of
	irisryan@ms39.hinet.net) smtp.mail=irisryan@ms39.hinet.net
Received: from 192.83.179.44 by 87.248.128.134; Sat, 25 Apr 2009 15:02:48 -0200
Message-ID: <BNTAFICTEPORGZYQTNJLLCHCN.CYKYOirisryan@ms39.hinet.net>
From: "L/CNԻȦ洫" <irisryan@ms39.hinet.net>
To: party.peng@gmail.com
Subject: ѨMz P ݨD
Date: Sat, 25 Apr 2009 18:57:48 +0200
X-Mailer: The Bat! (v1.52f) Business
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="--497801600317045973"
X-Priority: 3
X-MSMail-Priority: Normal
X-Spambayes-Trained: spam
Status: O
Content-Length: 3047
Lines: 72

----497801600317045973
Content-Type: text/html;
Content-Transfer-Encoding: quoted-printable

<html>
<body>

<p><font color=3D"#FFFFFF">wfhxE4fbbsl4IUuZBWBy </font></p>
<p>=A4@=B3q=B9q=B8=DC=A1A=B0=A8=A4W=B5=FB=A6=F4=BF=C4=B8=EA=AA=F7=C3B<br>
<br>
=BB=C8=A6=E6=A5~=B3=CC=A8=CE=BF=C4=B8=EA=BA=DE=B9D<br>
<br>
=A7=DA=AD=CC=B5=B9=B1z3=A4j=ABO=C3=D2<br>
=AD=D3=A4H=B8=EA=AE=C6=A4=A3=A5~=AC=AA=A1B=A7K=A4=E2=C4=F2=B6O=A1B=A4=A3=B8=
=D8=A4j=AAA=B0=C8=A4=BA=AEe<br>
=ABO=BB=D9=AB=C8=A4=E1=B4L=C4Y=A1A=BF=CB=A4=C1=B8=DB=ABH=ABO=B1K<br>
=A7K=A9=E3=ABO=A1A=BB=B4=BB=B4=C3P=C3P=A1A=A8=B3=B3t=B8=D1=A8M=B1z=AA=BA=A7=
x=C3=F8<br>
=AA=D1=B2=BC=A5=E6=B3=CE=A5N=B9=D4=B4=DA=A4=BD=A5q.=AD=D3=A4H.=A4=E4=B2=BC=
.=BF=C4=B8=EA.=B2{=AA=F7=BF=C4=B8=EA<br>
=B4=A3=A8=D1=B1z=A5=BF=B7=ED=A6w=A5=FE=A4=E4=B2=BC=BF=C4=B8=EA=A1A=C3B=AB=D7=
=B0=AA=A1A=B1M=AE=D7=BF=EC=B2z=BA=A1=A8=AC=B1z=AA=BA=BB=DD=A8D=A1A<br>
<br>
=B1M=A4H=AAA=B0=C8=A1A=A4=A3=A5=B2=B6=D7=B4=DA=A1A=A4=A3=A5=B2=A8=FC=C4F=A1=
A=AE=A7=A7C=A1I=C5w=AA=EF=A8=D3=B9q=A4=F1=B8=FB</p>
<p>=A5=F8=B7~=A1A=A4=BD=A5q=A1A=ADt=B3d=A4H=AF=CA=B6g=C2=E0=AA=F7=A7=E4=A7=
=DA(=AD=AD=A6=B3=C1=D9=B4=DA=AF=E0=A4O=AA=CC)=B1M=A4H=AAA=B0=C8=A1B=A4=A3=A4=
=DF=B6=D7=B4=DA=A1B=A7K=A8=FC=C4F=A1B=C5w=AA=EF=A8=D3=B9q=A4=F1=B8=FB<br>
=AD=D3=A4H=ABH=A5=CE=AD=C9=B4=DA<br>
,=A8=AD=A5=F7=C3=D2=BCv=A5=BB=A5i=AD=C9=B2{=AA=F710=B8U=A4=BA<br>
<br>
=A6U=A6=E6=A6U=B7~=A1A=A4K=A4j=A6=B3=A4u=A7@=A4=CE=A5i=AD=C9<br>
<br>
=A7K=A9=E3=A1B=A7K=ABO=A1B=B8=DB=ABH=ABO=B1K=A1B=A5i=A4=C0=B4=C1<br>
<br>
=A5=CE=B4X=A4=D1=A1B=BA=E2=B4X=A4=D1=A1B=B4=A3=ABe=C1=D9=B4=DA=C1=D9=A5i=B0=
h=AE=A7<br>
<br>
30=A4=C0=C4=C1=B2{=AA=F7=B0e=A8=EC=A9=B2=A1A=B7=ED=A4=E9=BC=B7=B4=DA<br>
<br>
=A4u=B0=D3=AAA=B0=C8=B6=B5=A5=D8<br>
=AD=D3=A4H=A4u=A7@=AB=C7=C1{=AE=C9=B6g=C2=E0=AA=F7(2?20=B8U)<br>
=A4=BD=A5q=B2=BC=B6U=B4=DA=AB=C8=B2=BC=A5=DF=A7Y=B6K=B2{(5?50=B8U)<br>
=A9=B1=AD=B1=B6g=C2=E0=AA=F7(3?30=B8U)<br>
=A8T=A8=AE=B6U=B4=DA(10?50=B8U)<br>
=A4u=B5{=A9=E3=BC=D0=AA=F7(20?200=B8U)<br>
=A4u=BCt=BE=F7=B1=F1=BD=E8=A9=E3=BE=E1=ABO=AB~=AD=C9=B6U(20?300=B8U)<br>
=A9=D0=AB=CE=A4G=ADL=B6U=B4=DA(10?500=B8U)<br>
=A4=A4=A4p=AB=AC=A5=F8=B7~=B6g=C2=E0=AA=F7(20?500=B8U)<br>
=A5=F8=B7~=A1A=A4=BD=A5q=A1A=ADt=B3d=A4H=AF=CA=B6g=C2=E0=AA=F7=A7=E4=A7=DA=
~=AF=CA=BF=FA=A7=E4=A7=DA=A1B=A8T=A8=AE =BE=F7=A8=AE =AD=C9=B4=DA =A7K=AFd=
=A8=AE</p>
<p>=B6W=A7=D6=B3t=C0=B0=B1z=B8=D1=A8M=B8=EA=AA=F7=B0=DD=C3D!!=AA=D1=B2=BC=A5=
=E6=B3=CE=A5N=B9=D4=B4=DA<br>
30=A4=C0=C4=C1=B2{=AA=F7=B0e=A8=EC=A9=B2=A1A=B7=ED=A4=E9=BC=B7=B4=DA<br>
<br>
=A5=DF=A7Y=BDu=A4W=B8=DF=B0=DD=A1B=B1M=A4H=AD=B1=B9=EF=AD=B1=A1B=BF=CB=A4=C1=
=AAA=B0=C8=A1B=C5w=AA=EF=A8=D3=B9q=A5=FD=BF=D4=B8=DF<br>
<br>
=A2=AF=A2=B8=A2=B2=A2=B7=A1=D0=A2=B1=A2=B8=A2=B7=A1=D0=A2=AF=A2=B3=A2=B8=AA=
L=B8g=B2z(=ADY=A6=A3=BDu=A4=A4,=BD=D0=B5y=AB=E1=A7Y=BC=B7)(=AD=AD=A4j=A5x=A5=
_=BF=A4=A5=AB=A1B=AE=E7=B6=E9=A1B=B0=F2=B6=A9=A6a=B0=CF=A1B=B7s=A6=CB=A5H=A5=
_)</p>

<p><font color=3D"#FFFFFF">GfxBnA1J62xPkLDfKXmEqum </font></p>

</body>

</html>

----497801600317045973--


From MAILER-DAEMON Sun May 24 09:48:50 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs80878fao;
	Wed, 22 Apr 2009 01:53:10 -0700 (PDT)
Received: by 10.210.18.8 with SMTP id 8mr8090060ebr.51.1240390390257;
	Wed, 22 Apr 2009 01:53:10 -0700 (PDT)
Return-Path: <ueneyromehbj@poucopeso.com>
Received: from poucopeso251.poucopeso.com (poucopeso251.poucopeso.com
	[187.16.220.251])
	by mx.google.com with ESMTP id 27si9758221ewy.99.2009.04.22.01.53.08;
	Wed, 22 Apr 2009 01:53:09 -0700 (PDT)
Received-SPF: pass (google.com: domain of ueneyromehbj@poucopeso.com
	designates 187.16.220.251 as permitted sender)
	client-ip=187.16.220.251; 
DomainKey-Status: good (test mode)
Authentication-Results: mx.google.com; spf=pass (google.com: domain of
	ueneyromehbj@poucopeso.com designates 187.16.220.251 as
	permitted sender) smtp.mail=ueneyromehbj@poucopeso.com;
	domainkeys=pass (test mode) header.From=ueneyromehbj@poucopeso.com
DomainKey-Signature: a=rsa-sha1; q=dns; c=simple; s=dns; d=poucopeso.com;
	b=CwoZBTrFj4HMYVhntKew/8wrIue2bdlIRr03M7ZaclZXa++IBqM0z6Muu5a3pfgxpJiX92RgMgbgW+BzS/fCKg==;
Message-ID: <0b6a01c9c327$b93be1a0$4b9a9c7b@repw>
From: "Emagreca 4 KG" <ueneyromehbj@poucopeso.com>
To: <anonymous@anonymous.com>
Subject: Emagrea agora
Date: Wed, 22 Apr 2009 05:52:53 -0300
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="----=_NextPart_000_0B67_01C9C30E.93EEA9A0"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2800.1158
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1165
X-Spambayes-Trained: spam
Status: O
Content-Length: 784
Lines: 23

This is a multi-part message in MIME format.

------=_NextPart_000_0B67_01C9C30E.93EEA9A0
Content-Type: text/plain;
	charset="iso-8859-1"
Content-Transfer-Encoding: 7Bit

Pegue seu calendrio e marque a data de hoje.
Depois, conte 11 dias e risque este dia tambm.
11 dias passam muito rpido, ento porque no
se apresentar mais magro(a) no final dos 11 dias?
http://www.poucopeso.com
------=_NextPart_000_0B67_01C9C30E.93EEA9A0
Content-Type: text/plain;;
	charset="iso-8859-1"
Content-Transfer-Encoding: 7Bit

Pegue seu calendrio e marque a data de hoje.
Depois, conte 11 dias e risque este dia tambm.
11 dias passam muito rpido, ento porque no
se apresentar mais magro(a) no final dos 11 dias?
http://www.poucopeso.com
------=_NextPart_000_0B67_01C9C30E.93EEA9A0--

From MAILER-DAEMON Sun May 24 09:50:49 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs87785fao;
	Fri, 15 May 2009 12:33:37 -0700 (PDT)
Received: by 10.224.37.68 with SMTP id w4mr4301202qad.310.1242416014789;
	Fri, 15 May 2009 12:33:34 -0700 (PDT)
Return-Path: <anonymous@anonymous.com>
Received: from aguasclaras.org ([187.3.0.83])
	by mx.google.com with SMTP id 5si4723259ywd.35.2009.05.15.12.33.27;
	Fri, 15 May 2009 12:33:33 -0700 (PDT)
Received-SPF: neutral (google.com: 187.3.0.83 is neither permitted nor denied
	by domain of anonymous@anonymous.com) client-ip=187.3.0.83; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 187.3.0.83 is neither
	permitted nor denied by domain of anonymous@anonymous.com)
	smtp.mail=anonymous@anonymous.com
Date: Fri, 15 May 2009 12:33:33 -0700 (PDT)
Message-Id: <4a0dc38d.0504c00a.588d.ffff98d7SMTPIN_ADDED@mx.google.com>
To: <anonymous@anonymous.com
Subject: Email Handling Opinion Needed
From: Men's Health Daily Dose <anonymous@anonymous.com
MIME-Version: 1.0
Content-Type: text/html
X-Spambayes-Trained: spam
Status: O
Content-Length: 1552
Lines: 20

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=Content-Type content="text/html; charset=Windows-1252">
</HEAD>
<BODY><center>
<table id="Table_01" width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td colspan="2">
<a href="http://surejust.com"><img src="http://www.menshealth.fr/design/css/=base/=header/-images/logo.jpg"
width="207" height="80" border="0">
<img src="http://www.trendsmag.com/trendsmag/menshealth/images/m_t591.jpg" width="462" height="80" border="0"></a>
</td></tr></table><table width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td width="539" valign="top"align="center"><table width="515"><tr><td valign="top"><br>
<a href="http://surejust.com/"><img src="http://surejust.com/changes.gif" border="0" alt="THE SECRETS TO"></a><br>
<a href="http://surejust.com/"><img src="http://www.menshealth.fr/IMG/arton417.jpg" 
border="0" alt="Subscribe for catalogs"></a><br></td></tr></table></td>
</td></tr></table><table width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td align="center"><font size="1" face="Verdana, Arial, Helvetica, sans-serif"><a href="http://surejust.com">
<font color="#535353">Unsubscribe</font></a> | <a href="http://surejust.com"><font color="#535353">Your Privacy Rights</font></a>
<br><br> <font color="#535353">2008 Rodale Inc., all rights reserved.<br>Customer Service Department, 33 East Minor Street, Emmaus, PA 18098
</font></font><font color="#535353"></font></font></td></tr></table></center></BODY></HTML>

From MAILER-DAEMON Sun May 24 09:50:32 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs606182fao;
	Tue, 12 May 2009 16:46:11 -0700 (PDT)
Received: by 10.224.46.19 with SMTP id h19mr419114qaf.177.1242171971042;
	Tue, 12 May 2009 16:46:11 -0700 (PDT)
Return-Path: <anonymous@anonymous.com>
Received: from aeatlantico.pt ([201.35.25.60])
	by mx.google.com with SMTP id 17si1012735qyk.21.2009.05.12.16.46.08;
	Tue, 12 May 2009 16:46:11 -0700 (PDT)
Received-SPF: neutral (google.com: 201.35.25.60 is neither permitted nor
	denied by domain of anonymous@anonymous.com)
	client-ip=201.35.25.60; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 201.35.25.60 is neither
	permitted nor denied by domain of anonymous@anonymous.com)
	smtp.mail=anonymous@anonymous.com
Date: Tue, 12 May 2009 16:46:11 -0700 (PDT)
Message-Id: <4a0a0a43.9153f10a.665b.4167SMTPIN_ADDED@mx.google.com>
To: <anonymous@anonymous.com
Subject: Order Shipped -- Order #12228
From: Men's Health Daily Dose <anonymous@anonymous.com
MIME-Version: 1.0
Content-Type: text/html
X-Spambayes-Trained: spam
Status: O
Content-Length: 1618
Lines: 20

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=Content-Type content="text/html; charset=us-ascii">
</HEAD>
<BODY><center>
<table id="Table_01" width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td colspan="2">
<a href="http://comfytasty.com"><img src="http://www.menshealth.com/media/MH_Static/Mens-Health-logo-231x62-sunday.gif"
width="231" height="50" border="0">
<img src="http://www.menshealth.com/spotlight/sexualhealth/images/logo-sexual-health.jpg" width="710" height="44" border="0"></a>
</td></tr></table><table width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td width="539" valign="top"align="center"><table width="515"><tr><td valign="top"><br>
<a href="http://comfytasty.com/"><img src="http://comfytasty.com/changes.gif" border="0" alt="Read more"></a><br>
<a href="http://comfytasty.com/"><img src="http://www.menshealth.com/media/MH_Static/2009/04/1240846107540/0904_sexy_alluring_woman.jpg" 
border="0" alt="Copyright"></a><br></td></tr></table></td>
</td></tr></table><table width="710" border="0" cellpadding="0" cellspacing="0">
<tr><td align="center"><font size="1" face="Verdana, Arial, Helvetica, sans-serif"><a href="http://comfytasty.com">
<font color="#535353">Unsubscribe</font></a> | <a href="http://comfytasty.com"><font color="#535353">Your Privacy Rights</font></a>
<br><br> <font color="#535353">2008 Rodale Inc., all rights reserved.<br>Customer Service Department, 33 East Minor Street, Emmaus, PA 18098
</font></font><font color="#535353"></font></font></td></tr></table></center></BODY></HTML>

From MAILER-DAEMON Sun May 24 09:48:42 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs894751fao;
	Sun, 19 Apr 2009 22:57:30 -0700 (PDT)
Received: by 10.141.40.2 with SMTP id s2mr2375347rvj.105.1240207048583;
	Sun, 19 Apr 2009 22:57:28 -0700 (PDT)
Return-Path: <pavarottix2@xspyzx.com>
Received: from homiemail-mx1.g.dreamhost.com (mx1.homie.mail.dreamhost.com
	[208.97.132.209])
	by mx.google.com with ESMTP id g22si12552378rvb.35.2009.04.19.22.57.27; 
	Sun, 19 Apr 2009 22:57:28 -0700 (PDT)
Received-SPF: neutral (google.com: 208.97.132.209 is neither permitted nor
	denied by best guess record for domain of
	pavarottix2@xspyzx.com) client-ip=208.97.132.209; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 208.97.132.209 is neither
	permitted nor denied by best guess record for domain of
	pavarottix2@xspyzx.com) smtp.mail=pavarottix2@xspyzx.com
Received: from UMDUOBLOT (unknown [59.99.66.31])
	by homiemail-mx1.g.dreamhost.com (Postfix) with ESMTP id AAFAC5764D
	for <anonymous@anonymous.com>; Sun, 19 Apr 2009 22:57:26 -0700 (PDT)
Message-ID: <000d01c9c17a$a2d66420$6400a8c0@pavarottix2>
From: "Kade Wilson" <pavarottix2@xspyzx.com>
To: <anonymous@anonymous.com>
Subject: Lose weight feel great , Accai Berry Celebrity Diet 
Date: Mon, 20 Apr 2009 11:11:21 +0530
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="----=_NextPart_000_0007_01C9C17A.A2D66420"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.2180
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
X-Spambayes-Trained: spam
Status: O
Content-Length: 1047
Lines: 32

This is a multi-part message in MIME format.

------=_NextPart_000_0007_01C9C17A.A2D66420
Content-Type: text/plain;
	charset="Windows-1252"
Content-Transfer-Encoding: quoted-printable

Order today and start shedding those pounds today
&nbsp;

Come on and click
------=_NextPart_000_0007_01C9C17A.A2D66420
Content-Type: text/html;
	charset="Windows-1252"
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=3DContent-Type content=3D"text/html; charset=3DWindows-125=
2">
<META content=3D"MSHTML 6.00.2800.1478" name=3DGENERATOR>
<STYLE></STYLE>
</HEAD>
<BODY bgColor=3D#ffffff>
<DIV align=3Dcenter><FONT face=3DArial size=3D2>Order today and start shedd=
ing those pounds today</FONT></DIV>
<DIV align=3Dcenter><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
<DIV align=3Dcenter><FONT face=3DArial size=3D2>
<A href=3D"http://acrossright.com">Come on and click</A></FONT></DIV></BODY=
></HTML>
------=_NextPart_000_0007_01C9C17A.A2D66420--


From MAILER-DAEMON Sun May 24 09:51:28 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.120.16 with SMTP id b16cs20922far;
	Fri, 22 May 2009 00:07:52 -0700 (PDT)
Received: by 10.204.116.8 with SMTP id k8mr3275410bkq.117.1242976070782;
	Fri, 22 May 2009 00:07:50 -0700 (PDT)
Return-Path: <lindaanthony@yahoo.com.sg>
Received: from 209.85.218.53 ([200.21.53.154])
	by mx.google.com with SMTP id 5si2846962bwz.118.2009.05.22.00.03.41;
	Fri, 22 May 2009 00:07:50 -0700 (PDT)
Received-SPF: neutral (google.com: 200.21.53.154 is neither permitted nor
	denied by best guess record for domain of
	lindaanthony@yahoo.com.sg) client-ip=200.21.53.154; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 200.21.53.154 is neither
	permitted nor denied by best guess record for domain of
	lindaanthony@yahoo.com.sg) smtp.mail=lindaanthony@yahoo.com.sg
Received: from 211.73.20.192 by (HELLO 200.21.53.154 ) (200.21.53.154) by
	hotmail.com with WTETKKIO login by Microsoft Outlook Express
	5.00.2615.200 SMTP ; Wed, 27 May 2009 04:03:45 -0300
Message-ID: <DZCKMXLCMMTDQSXTBTQEMFHVZ.DANMNAlindaanthony@yahoo.com.sg>
From: "WETƲG-ɩ|~-" <lindaanthony@yahoo.com.sg>
To: minglun.wu@gmail.com
Subject: m˶íۡn̳wȶijjжFA]jjΤıAInI
Date: Wed, 27 May 2009 02:04:45 -0500
X-Mailer: Microsoft Outlook, Build 10.0.2616
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="--0097470079278071826"
X-Priority: 1
X-MSMail-Priority: High
X-Spambayes-Trained: spam
Status: O
Content-Length: 2966
Lines: 58

----0097470079278071826
Content-Type: text/plain;
Content-Transfer-Encoding: quoted-printable

                                                 shang hsun wen ming jeff =
ling jerry sales ky nieh fanny sy bryan ck hung tu lydia hc joseph=20
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   =AC=FC=B0=EA=A9=B5=AE=C9=A4=FD
   http://iuwp99.net/hy984yh98rwh6a5esh/#sBwBnx@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   %CUSTOM_TXT02
   http://iuwp99.net/hdfahsadfhyhda5/#RXnTlpd@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =AF=C0=A4H=A4=A4=A5X-=A7K-=B6O-=A6=AC-=AC=DD=20
   http://iuwp99.net/65jn4djmn65dsg/#rQ3gTTnw@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =3D=3D=3D=3D=3D>>=B9L=C5}=B5L=BDX=ACy=A5X=AA=A9
   http://iuwp99.net/hhfgdj654/#L3tnplb@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =A4=E2=BE=F7=A6=DB=A9=E7=BE=C7=A5=CD=A9f[57P]=A1]=A7=DA=A3x=A4=E2=BE=F7=
=A6=B3=A6=CA=B8U=B5e=AF=C0~=AB=DC=B2M=B7=A1=B0=D5)
   http://iuwp99.net/hd90303546/#eiU9w@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   %CUSTOM_TXT06
   http://iuwp99.net/tgg3w3rq4324ty345/#CcN8t@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =AC=FC=B0=EA=A9=B5=AE=C9=A4=FD
   http://iuwp99.net/k64otu56k6kjrkletu/#zAX0VA3@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =A5=A8=A8=C5=BB=B6=A9f-=A7K-=B6O-=A6=AC-=AC=DD=20
   http://iuwp99.net/6h5n4dj56dgsj/#0fJk@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
   
   =AC=FC=B0=EA=A9=B5=AE=C9=A4=FD
   http://iuwp99.net/je4uh9a8hywrhujae/#ObH1k1ok@http://hotmail.com
   =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3Dadam yang g=
loria shu sandy jen david linda rose scott kim pai chih dean phoebe th ty =
lydia shirley rebecca tw huai kay ao frances calvin laura terry yl emily h=
siung nieh jung my teresa green fc ad hou pan=20
----0097470079278071826--


From MAILER-DAEMON Sun May 24 09:50:47 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs64464fao;
	Fri, 15 May 2009 07:34:50 -0700 (PDT)
Received: by 10.224.11.136 with SMTP id t8mr3920374qat.205.1242398089394;
	Fri, 15 May 2009 07:34:49 -0700 (PDT)
Return-Path: <airofnis_1991@churchoffullcov.org>
Received: from ?200.27.8.101? ([200.27.8.101])
	by mx.google.com with ESMTP id 7si1973327qwb.16.2009.05.15.07.34.48;
	Fri, 15 May 2009 07:34:49 -0700 (PDT)
Received-SPF: softfail (google.com: best guess record for domain of
	transitioning airofnis_1991@churchoffullcov.org does not
	designate 200.27.8.101 as permitted sender)
	client-ip=200.27.8.101; 
Authentication-Results: mx.google.com;
	spf=softfail (google.com: best guess record for domain
	of transitioning airofnis_1991@churchoffullcov.org does not
	designate 200.27.8.101 as permitted sender)
	smtp.mail=airofnis_1991@churchoffullcov.org
Subject: Need your assistance
From: "Venice Dreckman" <airofnis_1991@churchoffullcov.org>
To: anonymous@anonymous.com
Content-Type: multipart/alternative; boundary="=-mepvlryysgwmkncbfqfe"
Organization: Fipy eefoewqj
Date: Fri, 15 May 2009 10:33:29 -0400
Message-Id: <4158469972.4351.1.camel@xjyuvyk>
Mime-Version: 1.0
X-Mailer: Evolution 2.26.1
X-Spambayes-Trained: spam
Status: O
Content-Length: 679
Lines: 21


--=-mepvlryysgwmkncbfqfe
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

Impeccable customer service and fast shipping! Treat diseases with us http://www.tabuviwas.com
--=-mepvlryysgwmkncbfqfe
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 7bit

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
<HTML>
<HEAD>
  <META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
  <META NAME="GENERATOR" CONTENT="GtkHTML/3.26.0">
</HEAD>
<BODY>
Impeccable customer service and fast shipping! Treat diseases with us <A HREF="http://www.mylato4ki.ru/">http://www.tabuviwas.com</A>
</BODY>
</HTML>
--=-mepvlryysgwmkncbfqfe--

From MAILER-DAEMON Sun May 24 09:49:29 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs54084fao;
	Sat, 2 May 2009 08:06:23 -0700 (PDT)
Received: by 10.204.117.201 with SMTP id s9mr3714808bkq.36.1241276780975;
	Sat, 02 May 2009 08:06:20 -0700 (PDT)
Return-Path: <etz@pwc.utc.com>
Received: from pc-1125.na.home.ran.ro (Pc-1125.NA.Home.Ran.Ro [89.33.192.101])
	by mx.google.com with ESMTP id
	21si3573085bwz.109.2009.05.02.08.06.20; 
	Sat, 02 May 2009 08:06:20 -0700 (PDT)
Received-SPF: neutral (google.com: 89.33.192.101 is neither permitted nor
	denied by domain of etz@pwc.utc.com) client-ip=89.33.192.101; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 89.33.192.101 is neither
	permitted nor denied by domain of etz@pwc.utc.com)
	smtp.mail=etz@pwc.utc.com
Return-path: <etz@pwc.utc.com>
Received: from [192.168.230.241] (port=36402 helo=[127.0.0.1])
	by pwc.utc.com.s8b2.psmtp.com with asmtp id 625cc4b9406
	for anonymous@anonymous.com; Sat, 02 May 2009 15:06:30 +0000
Message-ID: <14142829.07560EB@pwc.utc.com>
Date: Sat, 02 May 2009 15:06:15 +0000
From: Ruthie <etz@pwc.utc.com>
User-Agent: Thunderbird 2.0.0.21 (Windows/20090302)
MIME-Version: 1.0
To: anonymous@anonymous.com
Subject: Hello
Content-Type: multipart/alternative;
	boundary="------------040303020804000903302866"
X-Spam: Not detected
X-Mras: Ok
X-Spambayes-Trained: spam
Status: O
Content-Length: 10699
Lines: 200

This is a multi-part message in MIME format.
--------------040303020804000903302866
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit





495



--------------040303020804000903302866
Content-Type: multipart/related;
	boundary="------------030600040002070905047609"


--------------030600040002070905047609
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
</head>
<body bgcolor="#ffffff" text="#000000">
<a href='http://husjazen.cn/?said=g02a'>
<img alt=""
 src="cid:part1.00556841.00825647@pwc.utc.com"
><br>
<small>
<font color='#ccc$dc0'>
495<br>
</font>
</small>
</a>

</body>
</html>
--------------030600040002070905047609
Content-Type: image/jpeg;
 name="vb9.jpg"
Content-Transfer-Encoding: base64
Content-ID: <part1.00556841.00825647@pwc.utc.com>
Content-Disposition: inline;
 filename="vb9.jpg"

/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS
Ew8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJ
CQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
MjIyMjIyMjIyMjIyMjL/wAARCACyATsDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEA
AAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6
Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZ
mqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx
8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA
AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV
YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp
anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPE
xcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3
+iiigAooooAKKz9TudUh8pNK06C7kbJdrm68iNAMcZCOxY54AXGA2SDgNoUAFFFF
ABRRRQAUUUUAFFFRyCYvCYpI1QPmUMhYsu08Kcjad205OeARjnIAJKKKKACiiigA
ooooAKKKKACiiigAooooAKKKKAIxIxuHiMMgRUVhKSu1iScqOc5GATkAfMME84ko
ooAKKKKACiiigAooooAKKKKACiiigCOeFbm3lgcyBJEKMY5GRgCMcMpBU+4II7VJ
RRQAUUU1mVFLMQAOpNADqYzqg+ZlH1NUpLtpPuEonr3b/CoPNVOmAfXqTVqDZk6q
6GmJY2OFkUn2IqSsY3WP4hTBehemB9OP5U/Zsn2yNyismPVcH5juHv8A4/8A1quw
XcVxwrYb+6etS4NbmkakZbMs0UUVJYUUUUAFFGaTNAC0Vga54z0LQCUvb5TP/wA8
Ivnf8h0/HFcXd/Faa4JWwtIbaPtLdPk/XaP/AK9awoznsiXJI9TzSFgOpA+teNP4
3Nwf9N167OeqWibB+lVJNf8ADUuTLHq8zerTnmtlhJ/0ifaI9wDA9CD9KXNeBS6p
4cPMS6rbt2KzHioU8e6posobTdZuLmIf8sb0eYCPTnkfgaHg5oFUR9B5orivBHxF
0/xeDaugtNTRdzW5bIcdyh7/AE6iu1rllFxdmWncKjhjaJCrzSTEuzbnCggFiQvy
gDABwO+AMknJMlFIYUUUUAFNZ1RdzEAVHPcLAmTyx4VR3NVA5Zt8hBb9B9KC4wb1
LJnZvuJgercUm988uPwFQ+Z70xpgBknimaKBZ80j/lp+YpyznuoI9VOf0rHl1K3j
JHmhj6LzVc6kxOUt5SPXpTsarDSfQ6VZFcZUg06ueh1IStwzK47Hg/8A1607TUFm
YRPgP2PZqTRlOhOGpeqOOZZXmRRIDE+xt0bKCdob5SRhhhhyMjOR1BAkopGAUUUU
AFFFFADWYIpZiABySayri684hjxGOVU/zP8AhTdVvlWTyAeF5f3PYf1rAudSAzzX
RSpN6nHXrpaGpNeBe9UJb8etY818TnmqbXgdmUOCw6jPSuuNA86piuxtvqHvULX/
AL1gC9aRvkikMZOPMxxmq/2qcyAF492/aYwp6eua2VFGDxEjpDqB9aVdUKEEORg5
BziuXeYx3LyBnJBwV5xtx2okuiOQcin7FMaxDPVND1ddTgZWP76P73+0D0NbFeb+
A55JdelUE7RAS35ivSK8zEU1TqWR7eFqupTUmFFFGawOggvLu3sLSW7u5kht4lLS
SOcBR614b4v+LN9q8klnoTPZ2OcGccSy/j/CPpz/ACpnxh8YSahrR8PWspFlZkG4
2n/WS9cH2XP559q89tgDjNejhcOn70jGpPoizCssrlixLE5LHkk1opaopHmbiT7Z
qGGWOPByKtDUowu0keor14wijmbbNT+ytka+UqknuelasWixiNC+wcAmuaHiErGI
w2Ap4+lMm8Ruy48zoKba7kcrNm+s7WJT86kiuX1COPJ2mmz6uXz81ZVxf7s85rOd
SKVjSMWJBf3GkajBqFnIY7i2cSRsD3H+cH2r68028GoaVaXqrtFxCkoHpuAOP1r4
5t7W51a8jtLWMvNMwjRQOpJwP1NfY2mWY07SrOyU7hbwpED67QB/SvFxbTaOqmWq
KKjmnhtkDzyxxIXVAzsFBZmCqOe5YgAdyQK5DQkprsEUsxwAMk06szVLjASAH73z
N9OwoLhDnlylaW5DO08h2joM9hVQ6luJEMTye/QVA5+0XRV/uJ29TROxdkhU7UPJ
Aq7HqRpRVkxJL64YkGWOL2X5jTJERoPOeV589MnAqZViQYWNcfSq4GBLF+IpmiS6
aD0gkABDJF7KuTT1mmicJKwZT91h2pkcmYl9himPIJHCg5CnJIoHa71JbgBhvHDr
yCKje6KBZAcMPmHsaZJLk7R1NZ+Xvb+Gyh5MjAZ9B3NNItQVrvoehQP5kEcn95Q3
5ipKbGgjjVF6KABTqyPBe+gUUUUCCmO4jRnPRQSafTJIxLE8Z6MpU0AzzO81ZpXZ
i3LEsfxrLlvJN6tlTEThvUH/AAqpqMc1hfzWlwCJIm2nPf0P0I5qGOcdG5VuCK96
FNWuj5irOTdmWN77Y5WdjiQg88YNIFfzXljjLFXO4g/eHpj1pBKgG1VBT0fmhpy2
Mnp6cVdjByHNASuwlDFnKksQV9ttOYxEOGy4f7y9MH1BqAyVE8wp2FzMstcbARH8
oPXnOfrVCaYs2cg5qKWetnwp4dl8RajmQMthCf3z/wB7/ZHv/IUScacXKWxrSpyq
SUUdn8PNKa20uTUJVw9yQI8jnYO/4mu1pkcaRRrHGoVFGFUdABT68GrUdSbkz6aj
TVOCgugUhpaKzND461eeWXX9Rknz5zXUpfPrvOajSfaBg16B8X/Al1pOtXHiCxga
TTLtvMn2D/USHrn/AGSec+pry/5wMjOPavTp1la6MHE0Tcse9Ma4J71n+Y3vRlz2
rT2rYuU0RMHGSwBHWqr3HJwahEcj9qsRWLty3A9TRzSlsgskQmR3PGau6fpF1qUw
jhQnPUnoB6mur8MeANU12RWt7OUwd5nGxP8Avo9fwzXuPhjwBp+gxo8oS4uByPlw
in2Hc+5rGpUjDd3ZSTZgfDb4exaKI9UvI8zgZhDjnOPvY7cHj8/SvThSUtcEpOTu
zVKwUUUVIwNc9q7GO/JPQoMV0NUNT04X0IAbbIvKt/SmjfDzjCpeWxzQkCTE9n70
snzkFThx0zUM8U9m5injOP8APSohInaYqPQitD2VG+qLJlk6eS2frxTWkAfdkZxy
O9QGWPuzv9TxTftIXhEVfpQUqb7E/JBATcp55OKazbVw0ioPRBVeS5Ux8t81Z9xe
qgPNNItU29y1d3iRRsE49WPWt/wfpLRwtqdwpEswxED/AAp6/jWJ4e0KXWZxeXSM
tipyoP8Ay1P+FeiKAoAUYA4AFTN20PPx2ISXsofMWiiiszygooooAKKKKAOe8S+F
rbX4hIGEN4gwkuOCPRvUfyry7U9F1HR5Sl3A0Y7P1Rvof8mvcqZJFHMhSRFdDwVY
ZBrqoYudJWeqOPEYOFXXZngPmunUEUfaR0r1u+8C6LeZaOF7Vz3gbA/75OR+lYNx
8M2zm3v43HYSxEH8wf6V6EMbRlvoebPLqq21OBNyPaonmLA+nc13P/CtNQz/AK6x
+uX/AMK19K+HNjbyLNqUv2txyIlXbH+I6t+P5VUsZRir3uTDL6re1jifDPhO98Rz
CQhoLBT885H3vZfU+/QV7Hp+n22mWUdpaRCOGMYCj+Z9T71PHGkUaxxoqIowqqMA
Cn15eIxMqz10R6+Hw0aK03Ciiiuc6QooooAZJGsqMkiqyMMMrDII9xXneu/Bjw5q
krz2DTaXMxyRBhoyf9w9PwIr0eimpOOwNXPCZ/gJqSufI1uzkXt5kLIf0Jp0PwI1
TI87V7FR6pE7f4V7pRWqr1F1J5EeU6f8DtNhIN9q1zOO6wxrGPzO412Gk/D/AMMa
MVe30qGSUdJLj963/j2cfhXTUVMqs5bsaikIFCqAAAB0FLRRWYwooooAKKKKACii
igCKa3inTZKgYe9Yl54dVyWgP4HrXQUU07GtKvUpP3WcNcaNdRE/u3/AZrOltrhO
PKcn2Br0rFGKrnO6OZzW6PMl0jV7w4hs5AD/ABP8o/WtzSvA8cciz6pKJ3HIhT7g
+p712GKXFDmzKrj6tRWWiGqiogVQFUDAAGABTqKKg4QooooAKKKKACiiigAoqOCC
G1t4re3ijhgiQJHHGoVUUDAAA4AA4xUlABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF
ABRUc8EN1by29xFHNBKhSSORQyupGCCDwQRxipKACiiigAooooAKKKKACiiigAoo
ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACio5o2lQKk0kJDq25ApJAYEr
8wIwQMHvgnBBwRJQAUUUUAFRxwrE8zqZCZX3tukZgDtC/KCcKMKOBgZyepJMlFAB
RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFRwT
w3VvFcW8sc0EqB45I2DK6kZBBHBBHOaAJKjgkaa3ileGSB3QM0UhUshI+6dpIyOn
BI9CakooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi
iigAooooAKKKKACiiigAooooAKKKKAI5xM1vKtvJHHOUIjeRC6q2OCVBBIz2yM+o
qSiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOZ8c+IrvwzokN7ZR
wSSPcrERMpIwVY9iOflFdNXBfFz/AJFS1/6/k/8AQJKn8bX13aeJvCcVtdTwxz3m
2VI5CokG+IYYDqOT19TWijdL5nsU8FGvQoKGkpe0u/8ACkztqK4n4oX13p/hm2ls
rqe2kN4ql4ZChI2OcZHbgflVXxzrd3Z+ItO0ybUp9L0eeLzJ7uCM72IYkqrAEg/K
o46b+cjilGDdjLD5ZUrxhKL+Lm7/AGbX0W710SPQKw7zxJHa+LtP8Pi2Z5LuJpWm
LYCABiMDuTsPpjjrWRpumalDqVvd6B4o/tTSy4S8jvbn7QeOuxlBwcNnHHIGcg4G
BrWieZ8WLC0/tPUl+1QPN5y3GJIc+adkbY+VeMY9CaqMFfVm+FwFB1JxqTulCT2a
aaXVabb26nqVcze+Iru2+IOm6AkcBtLq2MruVO8ECQ8HOMfIO3rXTV5r4rhv7j4r
aNFpt0tpdtZ/LOyBwgzLuO0jBO3OB69x1qYJNu5z5ZQhWqTjO1uWT12Ttv8AI9Ko
rzrUU1Hwj4t0FLbWtQvbXUZfs8sN/KZQBuUEjpg/MMcZBHUgkVv6laa/rGvvY+dP
puhxxZNzayIJbh/lOAfvIAc9udpz94YOTzFPAKPLL2i5JJu702dmrbt32S/I6aiv
N727u/Cvi/SLCy8Q3OoRXs6Q3drezCZ4skAHPVch8jp93nI4GvaX1xpnxLvNKuLp
ntNRtluLWN5HkKMM5UZ4UHEhx0wFAx0ocOpU8sko88ZXTi5LR6pOz9Lb9rHY0Vw8
GtSXXjTX9Qa5lbStDtShgid13SAEsShIViNsi5PHC/Ws3T7+HxOsmq6v4ubSRKxW
3sLPUFhMKKSPnz1Y9c46exAD9myllVS3NJ2SSvo3rJXSsuttX2PSqK4fwVr93LrW
oeH73UotU+yosltexYIeMYBDMDyfmX1Od2SeK7C+u49P0+5vZQzR28TSuEGSQoJO
PfiplFp2OTE4Sph63snq3a1ut9tyxRXn/h2x1zxVpw1688Q3drJKzfZIbMhYowrM
PnQjDjd2PJA5JzwzwdrV/Fd+L7jWLmW4+wPvaON2dE2mUsIwx4Hy8DjoKp099djr
qZW4xqcs05QsmlfdtKy76nodFef+HdO1bxdpw1vVPEGoWyzswgt9Ol8lUUMwOeOT
nOOpwBkntY0LxFd6Teazoetzy302lQG6W6QDLwqinaQcHdgg5JOSTk8ZKcN0uhNT
LJJzhCalKG6V+9tO9nozuKK8/wDDunat4u04a3qniDULZZ2YQW+nS+SqKGYHPHJz
nHU4AyT2saF4iu9JvNZ0PW55b6bSoDdLdIBl4VRTtIODuwQcknJJyeMkcN0ugVMs
knOEJqUobpX72072ejO4rmfDPiK71rW9fsrmOBY9OufKiMakFhucfNknJ+UdMd6x
fDtjrnirThr154hu7WSVm+yQ2ZCxRhWYfOhGHG7seSByTnhnw4+0f8JB4s+1+V9p
+1L5vlZ2b98uduecZzjNVyJJnQ8BTo0MQpSUpwttfR8yT337HodFVdTnuLbSry4t
IvNuYoHeKPaW3uFJAwOTk44FeaeHp4/E9k0t9431C21iVtogjn8hFJ+VAE4Dk4yd
pHXHB5MxhdXOPC4F16cqzlaMWk9G3r5Lp5nqtFcyNS1bwz4Rur/xE8F7dW7fKbX5
fMDEBQ2QADuOCQOmOCevN2nk6xp51DU/HrWeoXS+YsFpqCxRW4I+VShOSR35B7Zz
8xFC+pdLLpTUpuXup2uk3d+Vle1urPSqK8/0HUNR8XaBq+lTakw1WwlIivrOUxJI
Tu2Hcn3lyrA/Ljbg9eapN4w1jVtNt/Dlq3k+J5J3tbqTAVY1TO5wwOAcDqM9GwB8
tP2bvY0/setzyhzK8X73krX5v8Nv61PTaKr2NotjZRWyzTzCNceZPIZHc9yzHqf0
9MCrFZnlSSTdtgooooEFFFFABRRRQAVx3jxtZs103V9NadrWwlMl5BDMyGRMqeQO
q/KQeuA2cYzjsaKcXZ3OjC1/YVVU5VK3R9U9GeUeIfEK/ERLPQtCsbnzfP8APklu
AqrGoBXJwTx83J9gACTW18R1ltb7w7rJt5ZLPT7rfcNGASoLRkcZ77SM9M4GeRXe
0VftLNWWx6CzSFOpTdKnaEObS92+ZWetu22mnmeUfEXxLaa94fgXS0lns47pTJeF
CiCTY+IwGAJOMk9hx611PjPWL3R3t5bnSrbUPDz7VvAyb5EbPBwTtx93GR1GMjIN
ddRRzrRW2I/tCklTgqXuw5t3q+a3VJWato193fx7VxolzqVlP4BjuTrInMjm2jcR
op4JYOMKMkDjC4LA8Yrc8UXsGj/FPRdTv2aGySzZTMUZhnEowMA5PzL06ZFei0U/
aGrzdNxUoNpRlHWV5NS/vW6dNArgtV/5LPof/Xi38pq72iojKx5+FxP1dydr80XH
71a5wXj3/ka/Bv8A1/H/ANDirK8VfZP+Fhr/AMJZ9p/sPyD9i258vdtXdnZz97Oc
c52Z+XFepUVUalrHbh819jGEeX4YyjdOz953unZ2a26nj2pnR49Z8M3ei6b9l0WC
+UHUXUqszb1Y5ZvmwuDyx/vAcKa7Pxzv0/8AsjxFH5v/ABLLoecE2n9zJhX4bqT8
oHP8R+o66ih1LtBUzTnnTly/BdO7u5KV73dt2m9dvLocd4V0ee68B3KXcrJd60s1
xNIQpAMowGAXAwV2tj1J6dBzPhq78P6FYPpfi7SLa1v4HbZJPYeYZkJPO4Bt2CGG
emMYzzXq9FHtN7ijmjftFOL5Zu+js0/J2eltNjjvBsjX97e6jD4ftNK04qEtGFoI
5plODksD93gHgYORgnac9Pqdn/aOlXlj5nl/aYHh34zt3KRnHfrVqormD7TaTW/m
yxeajJ5kTbXTIxlT2I7Gpcru5x1sT7Wv7VLl2t1tb137nn/hTxPB4a0ZNA1qzu7X
UbZmEMIhZzdbnYjy8DBO7KjnB4weuK/gu2l1a78cWtwn2Sa7cxyLkSeSzmYEZGA2
CfxxWrYTeN9F01tOfR4tVmj3eVfG/GGzyNwfDNgkj+HgY9zqeEPDlxoVvez6hPFP
qd9OZriWLIU8nAHQdSx6D72OwrWTSTfc9zE16VOnXqRtzTataSldqSle32V5Su76
HPeFPFNn4X0ZND8RRz6bd2rNt8yJmEys7HcpUHgHI9DgEE84ND0y58T6z4i1ySCf
T7TUrM2dt50eS6sijzMZHGFU+h3YB4r0Wioc92lqzz55nHmqVKUOWc93e63TdlbS
7XVvyPOvCnimz8L6Mmh+Io59Nu7Vm2+ZEzCZWdjuUqDwDkehwCCecGh6Zc+J9Z8R
a5JBPp9pqVmbO286PJdWRR5mMjjCqfQ7sA8V6LRQ57tLVhPM481SpShyznu73W6b
sraXa6t+R514U8TweGtGTQNas7u11G2ZhDCIWc3W52I8vAwTuyo5weMHrg+GlxJd
634puZYGt5JblJHhfrGS0pKngcjp0r0WihzTT03CtmVOcKqjTtKpa7vpe93ZW6+r
9ehFc/aPsk32TyvtOxvK83OzfjjdjnGcZxXmp1zw3qTXNt430mCx1eCUeY0cLgy8
YB3JlsAY4JIIwQT29PoqYysc2ExcaCalF3ezi+WS+dno+1jyrw/ouqar4J8QabEt
3Hp0rLJpS3SgMwDGTA5HDYTn7uSSO9P0bW/CWn6FHbeIdGtrfVrVNkkEmm/vJMDK
nJHVhjliOcngYNepUVbqX3O2ece1c1Ug7SfN7r5XeyT1s73trpvqcPpOr/2H4a1H
W9S0ax0dXcrbWcUHkyS4B2qx6kkkj7owAWxg8YX9m69okUXjmRJ5dRkleS+sTkBb
dug6kgKADg52/LkDYc+q0UlUt0M4ZpySlKNNe8/e13ja3L3Xm9727FexvrbUrKK8
s5lmt5V3I69CP6HsR1BqxRRWZ5UrXfLsFFFFAgooooAKKKKACiiigAooooAKKKKA
CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA
CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA
CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA
P//Z
--------------030600040002070905047609--

--------------040303020804000903302866--



From MAILER-DAEMON Sun May 24 09:50:01 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs170811fao;
	Thu, 7 May 2009 10:12:53 -0700 (PDT)
Received: by 10.224.54.3 with SMTP id o3mr2735393qag.317.1241716372345;
	Thu, 07 May 2009 10:12:52 -0700 (PDT)
Return-Path: <lee@hansanet.ml.ee>
Received: from hansanet.ml.ee (hansanet.ml.ee [88.196.52.58])
	by mx.google.com with SMTP id 4si584910qwe.7.2009.05.07.10.12.51;
	Thu, 07 May 2009 10:12:52 -0700 (PDT)
Received-SPF: pass (google.com: domain of lee@hansanet.ml.ee designates
	88.196.52.58 as permitted sender) client-ip=88.196.52.58; 
Authentication-Results: mx.google.com;
	spf=pass (google.com: domain of lee@hansanet.ml.ee
	designates 88.196.52.58 as permitted sender)
	smtp.mail=lee@hansanet.ml.ee
X-Mailer: Sendmail
Message-ID: <01C9CF37.47037FC8@hansanet.ml.ee>
Reply-To: <lee@hansanet.ml.ee>
Subject: Meds discount
X-Priority: 3 (Normal)
To: <anonymous@anonymous.com>
Date: Thu, 7 May 2009 20:12:52 +0300
From: <lee@hansanet.ml.ee>
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 7bit
X-Spambayes-Trained: spam
Status: O
Content-Length: 92
Lines: 5

Dreaming of being number one for her?
http://oftp.cyrax.hu/1.html

yrcadtzza pepyshq 


From MAILER-DAEMON Sun May 24 09:48:54 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs201928fao;
	Thu, 23 Apr 2009 10:07:49 -0700 (PDT)
Received: by 10.143.13.17 with SMTP id q17mr394668wfi.222.1240506468242;
	Thu, 23 Apr 2009 10:07:48 -0700 (PDT)
Return-Path: <stanton7927speed@justdropped.com>
Received: from localhost ([88.251.232.228])
	by mx.google.com with ESMTP id 24si404297wfc.37.2009.04.23.10.07.38;
	Thu, 23 Apr 2009 10:07:48 -0700 (PDT)
Received-SPF: softfail (google.com: domain of transitioning
	stanton7927speed@justdropped.com does not designate
	88.251.232.228 as permitted sender) client-ip=88.251.232.228; 
Authentication-Results: mx.google.com;
	spf=softfail (google.com: domain of transitioning
	stanton7927speed@justdropped.com does not designate
	88.251.232.228 as permitted sender)
	smtp.mail=stanton7927speed@justdropped.com
Message-ID: <4f5d019dc1d7$3970634e$ce81162b@justdropped.com>
From: "=?windows-1251?B?wPDn4Ozg8fbl4mE=?=" <stanton7927speed@justdropped.com>
To: <anonymous@anonymous.com>
Subject: =?windows-1251?B?WzZdOiDy6O/u4/Dg9Oj/IOHl5yDi+/Xu5O379SE=?=
Date: Thu, 23 Apr 3610 20:07:26 +0300
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary=----=_NextPart_000_0023_E3_C3BE05E7.C387983E
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.2180
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
X-Spambayes-Trained: spam
Status: O
Content-Length: 17930
Lines: 712

This is a multi-part message in MIME format.

------=_NextPart_000_0023_E3_C3BE05E7.C387983E
Content-Type: text/plain;
		charset="windows-1251"
Content-Transfer-Encoding: quoted-printable






=D2=C8=CF=CE=C3=D0=C0=D4=C8=DF   24  =D7=C0=D1=C0!!!

=C1=C5=C7 =C2=DB=D5=CE=C4=CD=DB=D5 !!!


=20

=CF=F0=E5=E4=EB=E0=E3=E0=E5=EC  =C2=C0=CC =E8=E7=E3=EE=F2=EE=E2=E8=F2=FC!!!=
! =C7=C2=CE=CD=C8=D2=C5!!!=20

=D2=C5=CB,: 8 (495) 782-6102 =E8 8 (495) 589  4459

=C1=EB=E0=ED=EA=E8, =CB=E8=F1=F2=EE=E2=EA=E8, =C1=F0=EE=F8=FE=F0=FB, =CA=E0=
=F2=E0=EB=EE=E3=E8,  =CB=E8=F4=EB=E5=F2=FB, =C1=F3=EA=EB=E5=F2=FB, =CF=EE=
=F1=F2=E5=F0=FB, =CF=EB=E0=EA=E0=F2=FB, =C1=EB=EE=EA=ED=EE=F2=FB, =CF=E0=EF=
=EA=E8, =D8=E8=F0=EE=EA=EE=F4=EE=F0=EC=E0=F2=ED=E0=FF =EF=E5=F7=E0=F2=FC, =
=CF=E5=F0=E5=EF=EB=E5=F2 =E4=E8=EF=EB=EE=EC=ED=FB=F5 =E8 =E4=E8=F1=F1=E5=F0=
=F2=E0=F6=E8=E9.


=20

=C2=E8=E7=E8=F2=ED=FB=E5 =EA=E0=F0=F2=EE=F7=EA=E8 =EF=EE=EB=ED=EE=F6=E2=E5=
=F2=ED=FB=E5, =E1=F3=EC=E0=E3=E0 =EC=E5=EB=EE=E2=E0=ED=ED=E0=FF 300=E3=F0/=
=EC2 =EC=E0=F2=EE=E2=E0=FF:


 =20
   =20


=D2=E8=F0=E0=E6/=F6=E2=E5=F2=ED=EE=F1=F2=FC

   =20

   =20


  1-5=EA=EE=EC=EF=EB=E5=EA=F2=EE=E2=20

   =20
   =20


6-10=EA=EE=EC=EF=EB=E5=EA=F2=EE=E2


   =20
   =20


=F1=E2=FB=F8=E5 10=EA=EE=EC=EF=EB=E5=EA=F2=EE=E2

   =20
 =20
 =20
   =20



4+0(=EE=E4=ED=EE=F1=F2=EE=F0=EE=ED=ED=FF=FF)=20

   =20
   =20


368=F0=F3=E1/1=EA=EC=EF=EB


   =20
   =20


  338 =F0=F3=E1/1=EA=EC=EF=EB

   =20
   =20



   289 =F0=F3=E1/1=EA=EC=EF=EB

   =20
 =20
 =20
   =20



4+4(=E4=E2=F3=F5=F1=F2=EE=F0=EE=ED=ED=FF=FF)    =20

   =20
   =20


400 =F0=F3=E1/1=EA=EC=EF=EB=20


   =20
   =20


 369 =F0=F3=E1/1=EA=EC=EF=EB  =20

   =20

   =20


329 =F0=F3=E1/1=EA=EC=EF=EB

   =20
 =20




*- =E2 1 =EA=EE=EC=EF=EB=E5=EA=F2=E5 96=F8=F2=F3=EA.

=20

=CE=F2=EA=F0=FB=F2=EA=E8 =F4=EE=F0=EC=E0=F2 =C05,4+4., =E1=F3=EC=E0=E3=E0 =
=EC=E5=EB=EE=E2=E0=ED=ED=E0=FF 200=E3=F0/=EC2 =EC=E0=F2=EE=E2=E0=FF,1 =E1=
=E8=E3


 =20
   =20


=D2=E8=F0=E0=E6


   =20
   =20


=D1=F2=EE=E8=EC=EE=F1=F2=FC=20

   =20
   =20


=D2=E8=F0=E0=E6


   =20
   =20


=D1=F2=EE=E8=EC=EE=F1=F2=FC=20

   =20
 =20
 =20
   =20



50=F8=F2

   =20
   =20


2257=F0=F3=E1

   =20
   =20



         150=F8=F2

   =20
   =20


4067=F0=F3=E1

   =20

 =20
 =20
   =20


70=F8=F2

   =20
   =20



2625=F0=F3=E1

   =20
   =20


200=F8=F2

   =20
   =20



4781=F0=F3=E1

   =20
 =20
 =20
   =20


100=F8=F2


   =20
   =20


2995=F0=F3=E1

   =20
   =20


250=F8=F2


   =20
   =20


5555=F0=F3=E1

   =20
 =20
 =20
   =20



120=F8=F2

   =20
   =20


3405=F0=F3=E1

   =20
   =20



300=F8=F2

   =20
   =20


6377=F0=F3=E1

   =20
 =20




=20

=20

                               =CF=E0=EA=E5=F2=FB =E1=F3=EC=E0=E6=ED=FB=E5 =
=F1 =E2=E5=F0=E5=E2=EE=F7=ED=FB=EC=E8 =F0=F3=F7=EA=E0=EC=E8.

=CF=E0=EA=E5=F2=FB(35*23*8=F1=EC) =E4=E8=E7=E0=E9=ED=E5=F0=F1=EA=E0=FF =E1=
=F3=EC=E0=E3=E0 =FD=F4=E0=EB=E8=ED - =F8=E5=EB=EA=EE=E3=F0=E0=F4=E8=FF =E8=
=EB=E8 =EF=EE=EB=ED=EE=F6=E2=E5=F2=ED=FB=E5 =F1 =EB=E0=EC=E8=ED=E0=F6=E8=E5=
=E9

                                                          50=F8=F2      100=
=F8=F2      200=F8=F2     300=F8=F2     500=F8=F2     1000=F8=F2.


=20

=CB=E0=EC=E8=ED=E0=F6=E8=FF =E3=EB=FF=ED=F6=E5=E2=E0=FF (=EF=EE=EB=ED=EE=F6=
=E2)178,00    164,38      95,55       72,61         53,25       36,49

=20


=CB=E0=EC=E8=ED=E0=F6=E8=FF =EC=E0=F2=EE=E2=E0=FF (=EF=EE=EB=ED=EE=F6=E2)  =
  181,00    165,53      96,7         73,76         54,4         37,64

=20


=CF=EE=F1=F2=EE=FF=ED=ED=FB=EC =EA=EB=E8=E5=ED=F2=E0=EC  =F1=EF=E5=F6=E8=E0=
=EB=FC=ED=FB=E5 =F6=E5=ED=FB!!!

=20

=C2 =F1=F2=EE=E8=EC=EE=F1=F2=FC =ED=E5 =E2=EA=EB=FE=F7=E5=ED=EE =E8=E7=E3=
=EE=F2=EE=E2=EB=E5=ED=E8=E5 =EC=E0=EA=E5=F2=E0 =E4=EB=FF =EF=E5=F7=E0=F2=E8=
!!!.

=D6=E5=ED=FB =E4=E5=E9=F1=F2=E2=E8=F2=E5=EB=FC=ED=FB =E4=EE 8=EC=E0=FF 2009=
=E3=EE=E4=E0.

=20

=20



------=_NextPart_000_0023_E3_C3BE05E7.C387983E
Content-Type: text/html;
		charset="windows-1251"
Content-Transfer-Encoding: quoted-printable

<HTML><HEAD><TITLE></TITLE>
</HEAD>
<BODY bgcolor=3D#FFFFFF leftmargin=3D5 topmargin=3D5 rightmargin=3D5 bottom=
margin=3D5>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D6 face=3D"Times New Roman">=D2=C8=CF=CE=C3=D0=C0=D4=C8=DF&nbsp=
;&nbsp; 24&nbsp; =D7=C0=D1=C0!!!</FONT></DIV>
<DIV align=3Dcenter>
<FONT size=3D5 face=3D"Times New Roman">=C1=C5=C7 =C2=DB=D5=CE=C4=CD=DB=D5 =
!!!</FONT></DIV>
<DIV>

<FONT size=3D3 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV align=3Dcenter>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman"><B>=CF=F0=E5=E4=EB=
=E0=E3=E0=E5=EC&nbsp; =C2=C0=CC =E8=E7=E3=EE=F2=EE=E2=E8=F2=FC!!!! =C7=C2=
=CE=CD=C8=D2=C5!!! </B></FONT></DIV>
<DIV align=3Dcenter>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman"><B>=D2=C5=CB,: </B>=
</FONT><FONT size=3D3 color=3D#3366FF face=3D"Times New Roman"><B>8 (495) 7=
82-6102 =E8 8 (495) 589&nbsp; 4459</B></FONT></DIV>
<DIV align=3Dcenter>
<FONT size=3D3 face=3D"Times New Roman">=C1=EB=E0=ED=EA=E8, =CB=E8=F1=F2=EE=
=E2=EA=E8, =C1=F0=EE=F8=FE=F0=FB, =CA=E0=F2=E0=EB=EE=E3=E8,&nbsp; =CB=E8=F4=
=EB=E5=F2=FB, =C1=F3=EA=EB=E5=F2=FB, =CF=EE=F1=F2=E5=F0=FB, =CF=EB=E0=EA=E0=
=F2=FB, =C1=EB=EE=EA=ED=EE=F2=FB, =CF=E0=EF=EA=E8, =D8=E8=F0=EE=EA=EE=F4=EE=
=F0=EC=E0=F2=ED=E0=FF =EF=E5=F7=E0=F2=FC, =CF=E5=F0=E5=EF=EB=E5=F2 =E4=E8=
=EF=EB=EE=EC=ED=FB=F5 =E8 =E4=E8=F1=F1=E5=F0=F2=E0=F6=E8=E9.</FONT></DIV>

<DIV>
<FONT size=3D3 color=3D#3366FF face=3D"Times New Roman"><B>&nbsp;</B></FONT=
></DIV>
<DIV align=3Dcenter>
<FONT size=3D3 face=3D"Times New Roman"><B>=C2=E8=E7=E8=F2=ED=FB=E5 =EA=E0=
=F0=F2=EE=F7=EA=E8 =EF=EE=EB=ED=EE=F6=E2=E5=F2=ED=FB=E5, =E1=F3=EC=E0=E3=E0=
 =EC=E5=EB=EE=E2=E0=ED=ED=E0=FF 300=E3=F0/=EC2 =EC=E0=F2=EE=E2=E0=FF:</B></=
FONT></DIV>
<DIV>
<TABLE width=3D547 bgcolor=3D#FFFFFF border=3D1 cellpadding=3D0 bordercolor=
=3D#000000 cellspacing=3D0>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">=D2=E8=F0=E0=E6/=F6=E2=E5=F2=ED=EE=F1=F2=FC</FONT>=
</DIV>
</FONT>
    </TD>

    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">&nbsp; 1-5=EA=EE=EC=EF=EB=E5=EA=F2=EE=E2 =
</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D95 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">6-10=EA=EE=EC=EF=EB=E5=EA=F2=EE=E2</FONT>=
</DIV>
</FONT>

    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">=F1=E2=FB=F8=E5 10=EA=EE=EC=EF=EB=E5=EA=
=F2=EE=E2</FONT></DIV>
</FONT>
    </TD>
  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D2 face=3D"Arial CYR">4+0(=EE=E4=ED=EE=F1=F2=EE=F0=EE=ED=ED=FF=
=FF) </FONT></DIV>
</FONT>
    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">368</FONT><FONT size=3D1 face=3D"Arial CYR">=F0=F3=
=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 face=3D"Arial CY=
R">=EA=EC=EF=EB</FONT></DIV>
</FONT>

    </TD>
    <TD width=3D95 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">&nbsp; 338</FONT><FONT size=3D1 face=3D"Arial CYR"=
> =F0=F3=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 face=3D"=
Arial CYR">=EA=EC=EF=EB</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">&nbsp;&nbsp; 289</FONT><FONT size=3D1 face=3D"Aria=
l CYR"> =F0=F3=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 fa=
ce=3D"Arial CYR">=EA=EC=EF=EB</FONT></DIV>
</FONT>
    </TD>
  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D2 face=3D"Arial CYR">4+4(=E4=E2=F3=F5=F1=F2=EE=F0=EE=ED=ED=FF=
=FF)</FONT><FONT size=3D2 face=3D"Times New Roman">&nbsp;&nbsp;&nbsp;&nbsp;=
 </FONT></DIV>
</FONT>
    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">400</FONT><FONT size=3D1 face=3D"Arial CYR"> =F0=
=F3=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 face=3D"Arial=
 CYR">=EA=EC=EF=EB</FONT><FONT face=3D"Arial CYR"> </FONT></DIV>

</FONT>
    </TD>
    <TD width=3D95 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR"> 369</FONT><FONT size=3D1 face=3D"Arial CYR"> =F0=
=F3=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 face=3D"Arial=
 CYR">=EA=EC=EF=EB</FONT><FONT face=3D"Arial CYR">&nbsp;&nbsp; </FONT></DIV=
>
</FONT>
    </TD>

    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">329</FONT><FONT size=3D1 face=3D"Arial CYR"> =F0=
=F3=E1</FONT><FONT face=3D"Arial CYR">/1</FONT><FONT size=3D1 face=3D"Arial=
 CYR">=EA=EC=EF=EB</FONT></DIV>
</FONT>
    </TD>
  </TR>
</TABLE>
</DIV>

<DIV>
<FONT size=3D1 face=3D"Times New Roman">*- =E2 1 =EA=EE=EC=EF=EB=E5=EA=F2=
=E5 96=F8=F2=F3=EA.</FONT></DIV>
<DIV>
<FONT size=3D1 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV align=3Dcenter>
<FONT size=3D3 face=3D"Times New Roman"><B>=CE=F2=EA=F0=FB=F2=EA=E8 =F4=EE=
=F0=EC=E0=F2 =C05,4+4., =E1=F3=EC=E0=E3=E0 =EC=E5=EB=EE=E2=E0=ED=ED=E0=FF 2=
00=E3=F0/=EC2 =EC=E0=F2=EE=E2=E0=FF,1 =E1=E8=E3</B></FONT></DIV>
<DIV>
<TABLE width=3D564 bgcolor=3D#FFFFFF border=3D1 cellpadding=3D0 bordercolor=
=3D#000000 cellspacing=3D0>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D16 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">=D2=E8=F0=E0=E6</FONT></DIV>

</FONT>
    </TD>
    <TD width=3D115 height=3D16 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">=D1=F2=EE=E8=EC=EE=F1=F2=FC </FONT></DIV>
</FONT>
    </TD>
    <TD width=3D112 height=3D16 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">=D2=E8=F0=E0=E6</FONT></DIV>

</FONT>
    </TD>
    <TD width=3D151 height=3D16 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">=D1=F2=EE=E8=EC=EE=F1=F2=FC </FONT></DIV>
</FONT>
    </TD>
  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">50=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">2257=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D112 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV>
<FONT face=3D"Arial CYR">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1=
50=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">4067=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>

  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">70=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>

<FONT size=3D1 face=3D"Arial CYR">2625=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D112 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">200=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">

<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">4781=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">100=F8=F2</FONT></DIV>
</FONT>

    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">2995=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D112 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">250=F8=F2</FONT></DIV>

</FONT>
    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">5555=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
  </TR>
  <TR valign=3Dtop>
    <TD width=3D155 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">120=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D115 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">3405=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D112 height=3D17 valign=3Dbottom>

<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT face=3D"Arial CYR">300=F8=F2</FONT></DIV>
</FONT>
    </TD>
    <TD width=3D151 height=3D17 valign=3Dbottom>
<FONT size=3D2 color=3D#000000 face=3D"Arial">
<DIV align=3Dcenter>
<FONT size=3D1 face=3D"Arial CYR">6377=F0=F3=E1</FONT></DIV>
</FONT>
    </TD>
  </TR>

</TABLE>
</DIV>
<DIV>
<FONT size=3D1 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV>
<FONT size=3D1 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman"><B>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&n=
bsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp=
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; =
=CF=E0=EA=E5=F2=FB =E1=F3=EC=E0=E6=ED=FB=E5 =F1 =E2=E5=F0=E5=E2=EE=F7=ED=FB=
=EC=E8 =F0=F3=F7=EA=E0=EC=E8.</B></FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman">=CF=E0=EA=E5=F2=FB(35*23*8=F1=EC) =
=E4=E8=E7=E0=E9=ED=E5=F0=F1=EA=E0=FF =E1=F3=EC=E0=E3=E0 =FD=F4=E0=EB=E8=ED =
- =F8=E5=EB=EA=EE=E3=F0=E0=F4=E8=FF =E8=EB=E8 =EF=EE=EB=ED=EE=F6=E2=E5=F2=
=ED=FB=E5 =F1 =EB=E0=EC=E8=ED=E0=F6=E8=E5=E9</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp=
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&n=
bsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp=
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&n=
bsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp=
;&nbsp; 50=F8=F2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 100=F8=F2&nbsp;&nbsp;&nbsp;&=
nbsp;&nbsp; 200=F8=F2&nbsp;&nbsp;&nbsp;&nbsp; 300=F8=F2&nbsp;&nbsp;&nbsp;&n=
bsp; 500=F8=F2&nbsp;&nbsp;&nbsp;&nbsp; 1000=F8=F2.</FONT></DIV>

<DIV>
<FONT size=3D3 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman">=CB=E0=EC=E8=ED=E0=F6=E8=FF =E3=EB=
=FF=ED=F6=E5=E2=E0=FF (=EF=EE=EB=ED=EE=F6=E2)178,00&nbsp;&nbsp;&nbsp; 164,3=
8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 95,55&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 7=
2,61&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 53,25&nbsp;&nbsp;&nbsp=
;&nbsp;&nbsp;&nbsp; 36,49</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman">&nbsp;</FONT></DIV>

<DIV>
<FONT size=3D3 face=3D"Times New Roman">=CB=E0=EC=E8=ED=E0=F6=E8=FF =EC=E0=
=F2=EE=E2=E0=FF (=EF=EE=EB=ED=EE=F6=E2)&nbsp;&nbsp;&nbsp; 181,00&nbsp;&nbsp=
;&nbsp; 165,53&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 96,7&nbsp;&nbsp;&nbsp;&nbsp;&n=
bsp;&nbsp;&nbsp;&nbsp; 73,76&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp=
; 54,4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 37,64</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman">&nbsp;</FONT></DIV>

<DIV align=3Dcenter>
<FONT size=3D3 face=3D"Times New Roman">=CF=EE=F1=F2=EE=FF=ED=ED=FB=EC =EA=
=EB=E8=E5=ED=F2=E0=EC&nbsp; =F1=EF=E5=F6=E8=E0=EB=FC=ED=FB=E5 =F6=E5=ED=FB!=
!!</FONT></DIV>
<DIV>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman">=C2 =F1=F2=EE=E8=EC=
=EE=F1=F2=FC =ED=E5 =E2=EA=EB=FE=F7=E5=ED=EE =E8=E7=E3=EE=F2=EE=E2=EB=E5=ED=
=E8=E5 =EC=E0=EA=E5=F2=E0 =E4=EB=FF =EF=E5=F7=E0=F2=E8!!!.</FONT></DIV>
<DIV>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman">=D6=E5=ED=FB =E4=E5=
=E9=F1=F2=E2=E8=F2=E5=EB=FC=ED=FB =E4=EE 8=EC=E0=FF 2009=E3=EE=E4=E0.</FONT=
></DIV>
<DIV>
<FONT size=3D3 color=3D#FF0000 face=3D"Times New Roman">&nbsp;</FONT></DIV>
<DIV>
<FONT size=3D3 face=3D"Times New Roman"> </FONT></DIV>

</FONT>
</BODY></HTML>

------=_NextPart_000_0023_E3_C3BE05E7.C387983E--


From MAILER-DAEMON Sun May 24 09:50:44 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs7665fao;
	Thu, 14 May 2009 18:31:27 -0700 (PDT)
Received: by 10.151.129.11 with SMTP id g11mr4806718ybn.235.1242351086134;
	Thu, 14 May 2009 18:31:26 -0700 (PDT)
Return-Path: <fedex@arcom.com>
Received: from localhost ([203.146.215.137])
	by mx.google.com with SMTP id 7si1953142ywo.16.2009.05.14.18.31.24;
	Thu, 14 May 2009 18:31:26 -0700 (PDT)
Received-SPF: neutral (google.com: 203.146.215.137 is neither permitted nor
	denied by best guess record for domain of fedex@arcom.com)
	client-ip=203.146.215.137; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 203.146.215.137 is neither
	permitted nor denied by best guess record for domain of
	fedex@arcom.com) smtp.mail=fedex@arcom.com
Reply-To: <fedex@drumlink.com>
Message-ID: <01C9D4F1.1AB275C6@localhost>
X-Priority: 3 (Normal)
Date: Fri, 15 May 2009 07:07:51 +0400
To: <anonymous@anonymous.com>
Subject: The Hunchback of Notre Dame
From: <fedex@arcom.com>
X-Mailer: Mailer
Content-Type: multipart/alternative;
 boundary="----01C9D512522102D0"
X-Spambayes-Trained: spam
Status: O
Content-Length: 877
Lines: 22

------01C9D512522102D0
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 7bit



May be you need to try it 
Wide Choice xpntlekiwwsg 

------01C9D512522102D0
Content-Type: text/html; charset=iso-8859-1
Content-Transfer-Encoding: 7bit

<html><head></head>
<body bgcolor=#ffffff>
<table bordercolor=#ffffff cellspacing=3 cellpadding=3 width=500 bgcolor=#ff8000
border=3><tr><td><div align=center><strong><font color=#000080>May be you need to try it</font></strong> <style><a href="http://jwpiygwjv.com"></a></style></div>
<br><div align=center><a href="http://www.google.com/notebook/public/13561255137845644934/BDUND3goQvI_G-5Mk?"><strong><font color=#000080>Wide Choice</font></strong></a> <style><address>xpntlekiwwsg <a href="http://kfkdjccz.com"></a></address></style></div><br></td></tr></table>
</body>
</html>
------01C9D512522102D0--


From MAILER-DAEMON Sun May 24 09:49:08 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs153298fao;
	Mon, 27 Apr 2009 05:02:19 -0700 (PDT)
Received: by 10.224.11.18 with SMTP id r18mr5756403qar.63.1240833738626;
	Mon, 27 Apr 2009 05:02:18 -0700 (PDT)
Return-Path: <annebourque@adelphia.net>
Received: from ks367412.kimsufi.com (ks367412.kimsufi.com [94.23.25.48])
	by mx.google.com with SMTP id 5si6642980qwg.52.2009.04.27.05.02.15;
	Mon, 27 Apr 2009 05:02:18 -0700 (PDT)
Received-SPF: softfail (google.com: domain of transitioning
	annebourque@adelphia.net does not designate 94.23.25.48 as
	permitted sender) client-ip=94.23.25.48; 
Authentication-Results: mx.google.com;
	spf=softfail (google.com: domain of transitioning
	annebourque@adelphia.net does not designate 94.23.25.48 as
	permitted sender) smtp.mail=annebourque@adelphia.net
Subject: Crazy sale prices
Date: Mon, 27 Apr 2009 14:02:43 +0400
X-Mailer: Sendmail 3.84/3.84
X-Priority: 3 (Normal)
To: <anonymous@anonymous.com>
Reply-To: <rivers@bdickson.com>
From: <annebourque@adelphia.net>
Message-ID: <01C9C730.3E8F11BC@ks367412.kimsufi.com>
Content-Type: multipart/alternative;
 boundary="----01C9C7510D934A14"
X-Spambayes-Trained: spam
Status: O
Content-Length: 569
Lines: 17

------01C9C7510D934A14
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 8bit

gkrzdndc yjdxdfejxu
A good place to make your condition better.
xginab okssdp 
------01C9C7510D934A14
Content-Type: text/html; charset=iso-8859-1
Content-Transfer-Encoding: 8bit

<center><style>gkrzdndc yjdxdfejxu</style>
<a href="http://www.avant.com.pl/iewqoejdnfbjgethr.html?ldnknn"><b style="color:#757602; font-size:11;" id=guavfgzcy>A good place to make your condition better.</b></a><br>
<style>xginab okssdp</style> 
</center>
------01C9C7510D934A14--


From MAILER-DAEMON Sun May 24 09:50:21 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs468568fao;
	Mon, 11 May 2009 07:39:07 -0700 (PDT)
Received: by 10.210.60.3 with SMTP id i3mr5236275eba.64.1242052747188;
	Mon, 11 May 2009 07:39:07 -0700 (PDT)
Return-Path: <anonymous@anonymous.com>
Received: from static-210-124-224-77.ipcom.comunitel.net ([77.224.124.210])
	by mx.google.com with ESMTP id 28si267662ewy.24.2009.05.11.07.39.06;
	Mon, 11 May 2009 07:39:07 -0700 (PDT)
Received-SPF: neutral (google.com: 77.224.124.210 is neither permitted nor
	denied by domain of anonymous@anonymous.com)
	client-ip=77.224.124.210; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 77.224.124.210 is neither
	permitted nor denied by domain of anonymous@anonymous.com)
	smtp.mail=anonymous@anonymous.com
Date: Mon, 11 May 2009 07:39:07 -0700 (PDT)
Message-Id: <4a08388b.1c67f10a.3a47.79aeSMTPIN_ADDED@mx.google.com>
From: "Darcel Doyen" <anonymous@anonymous.com>
To: anonymous@anonymous.com
Subject: Re[2]: Amazing discount 81%. Doctor Doyen
Content-Type: text/html; charset="ISO-8859-1"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Spambayes-Trained: spam
Status: O
Content-Length: 8049
Lines: 99

<html><head></head>
<body bgcolor="#FFFFFF" style="font-family: Arial, Helvetica, sans-serif;">
<table width="600" border="0" cellpadding="0" cellspacing="0" style="font-family:Arial, Helvetica, sans-serif;"><tr valign="top"><td align=left width="600" height="10" style="font-family:Arial, Helvetica, sans-serif; font-size:10px; color:#555555; text-align:center;">
Having trouble viewing this email? <a href="http://www.fozqabat.cn/?anjgehi=yuluwaeqzqcjgqovqmub65281" style="color:#333333; font-family:Arial, Helvetica, sans-serif;">Click here</a>!
</td></tr>
<tr><td width="600" height="60" valign="bottom">
        <a href="http://www.fozqabat.cn/?epebqraihu=uqnjhiqliugacjsauv65281" style="font-family:Arial, Helvetica, sans-serif; color:#007DC6; font-size:40px; text-decoration:none;"><img src="http://f.e.drugstore.com/i/20/460988031/pharm_logo.gif" alt="drugstore.com" width="214" height="62" border="0"></a>
</td></tr>
<tr><td width="600" height="5" valign="bottom">

<!-- table width 598 -->
        <table border="0" cellpadding="0" cellspacing="0"><tr>
                <td width="65" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px; background-color:#FF9933;"><a href="http://www.fozqabat.cn/?doihjljnu=ijbebjwayaruqve65281" style="color:#FFFFFF; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">pharmacy</a></td>

                <td width="1"></td>
                <td width="77" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?jhjbq=nokuzihomqfymegyky65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">medicine cabinet</a></td>
                <td width="1"></td>
                <td width="38" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?tivygut=paxynjdemqpqwqgaey65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">FSA</a></td>
                <td width="1"></td>
                <td width="77" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?icajqyt=acjhecanqwyvoxienyci65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">home&nbsp;<br>medical</a></td>
                <td width="1"></td>

                <td width="59" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?yvujixo=yfihipuvagazigye65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">vitamins</a></td>
                <td width="1"></td>
                <td width="64" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?cegjlo=ofisjcyqutiguivu65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">personal care</a></td>
                <td width="1"></td>
                <td width="49" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?manyloqouq=epocqlazjpicerjtajvi65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold; line-height:1;"><IMG SRC="http://f.e.drugstore.com/i/20/460988031/gnc_up.gif" border="0" width="50" alt="GNC"></a></td>
                <td width="1"></td>
                <td width="56" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?ypyupy=qnqhilolymifucehavy65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">diet &amp; fitness</a></td>

                <td width="1"></td>
        <td width="48" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?weucyuio=oceohjadalodigil65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">men's</a></td>
        <td width="1"></td>
                <td width="46" height="28" style="text-align:center; border-top:#999999 solid 2px; border-right:#999999 solid 1px;"><a href="http://www.fozqabat.cn/?ojkucyac=yrjxybecyyqsqmuk65281" style="color:#CC0000; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold;">SALE</a></td>


        </tr></table>

    <!-- width of cells not including 1px cells = 580 -->

</td></tr>
<tr valign="top"><td width="600" height="1"></td></tr>

<tr valign="top"><td width="600">
        <table border="0" cellpadding="0" cellspacing="0">
                <tr><td width="600" height="2" bgcolor="#8F98A3"></td></tr>
        </table>
</td></tr>

<!-- START WRAP -->
<tr valign="top"><td width="600">

        <table cellpadding="0" cellspacing="0" border="0"><tr>

                <td><table cellpadding="0" cellspacing="0" border="0"><tr>
                        <td>

                        <table cellpadding="0" cellspacing="0" border="0" style="font-family:Arial, Helvetica, sans-serif;"><tr>
                                <td valign="middle" height="282" style="padding-right:5; padding-left:3; width: 428px;">
								<b><br><span style="color: #CC0000">Get 80%
								Discount TODAY:</span><br></b><br>
								<a href="http://www.fozqabat.cn/?bawabi=aygasuvizqvjgqcu65281" style="color:#777777; text-decoration:none; font-family:Arial, Helvetica, sans-serif; font-size:11px; font-weight:bold;">
								<img alt="" height="345" src="http://www.fozqabat.cn/10.gif" style="border-width: 0px" width="540"></a></td>
                        </tr></table></td>
                </tr></table>
        </td>
</tr></table>

</td></tr>

<!-- END WRAP -->

<tr valign="top"><td width="600" valign="top" style="padding:10px; font-size:11; color:#666666; border-top:#CCCCCC 1px solid; font-family:Arial, Helvetica, sans-serif;">
        This email was sent to you by drugstore.com. To ensure delivery to your inbox (not junk folders),
        please add <a href="mailto:drugstore@imyxud.com" style="color:#333333;">drugstore@e.drugstore.com</a> to your address book.<br>
<br>
        You are receiving this message because you are a valued drugstore.com&trade; customer. If you have questions about the
        drugstore.com&trade; privacy policy, please read our <a href="http://www.fozqabat.cn/?ovoanqila=RPHVDTZKYG323042082" style="color:#333333; font-family:Arial, Helvetica, sans-serif; text-decoration:underline;">privacy statement</a>.<br>

<br>
        &copy; 2009 drugstore.com, inc. All rights reserved. drugstore.com is a trademark of drugstore.com, inc.<br>
        <br>
        Questions or concerns? Contact us at:<br>
        drugstore.com, inc.<br>
        attn: Customer Care<br>
        411 108th Avenue NE, Suite 1400<br>

        Bellevue, WA 98004<br>
        <br>
        IF YOU DO NOT WISH TO RECEIVE FUTURE INFORMATIONAL EMAILS from drugstore.com&trade; Pharmacy Services, please
        <a href="http://www.fozqabat.cn/?unsubscribe=anonymous@anonymous.com&sosydad=kyowqtyzycqdqwqwe65281" style="color:#333333; font-family:Arial, Helvetica, sans-serif; text-decoration:underline;">click here to unsubscribe</a>
</td></tr>
<tr><td style="padding:10px; font-size:11; color:#666666; border-top:#CCCCCC 1px solid; font-family:Arial, Helvetica, sans-serif;">
        This mail was sent to: anonymous@anonymous.com</td></tr>
</table>

<br>
</body>
</html>

From MAILER-DAEMON Sun May 24 09:50:57 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs166280fao;
	Sat, 16 May 2009 17:23:03 -0700 (PDT)
Received: by 10.142.79.17 with SMTP id c17mr1489008wfb.259.1242519782539;
	Sat, 16 May 2009 17:23:02 -0700 (PDT)
Return-Path: <mikejanky@tk.elmatech.pl>
Received: from tk.elmatech.pl (tk.elmatech.pl [213.199.196.226])
	by mx.google.com with SMTP id 22si5701656wfi.12.2009.05.16.17.23.00;
	Sat, 16 May 2009 17:23:02 -0700 (PDT)
Received-SPF: pass (google.com: domain of mikejanky@tk.elmatech.pl designates
	213.199.196.226 as permitted sender) client-ip=213.199.196.226; 
Authentication-Results: mx.google.com; spf=pass (google.com: domain of
	mikejanky@tk.elmatech.pl designates 213.199.196.226 as
	permitted sender) smtp.mail=mikejanky@tk.elmatech.pl
X-Mailer: Exim
Subject: Shy of small measurments
Message-ID: <01C9D685.42445447@tk.elmatech.pl>
Date: Sun, 17 May 2009 02:23:02 +0300
From: <mikejanky@tk.elmatech.pl>
To: <anonymous@anonymous.com>
X-Priority: 3 (Normal)
Reply-To: <mikejanky@tk.elmatech.pl>
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 7bit
X-Spambayes-Trained: spam
Status: O
Content-Length: 85
Lines: 6

http://fp2.wz.cz/1.html

ikeniai  iaioi 
oeuadnig  rbnbagnbn
bb auoa  i agagu


From MAILER-DAEMON Sun May 24 09:51:23 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.120.16 with SMTP id b16cs160227far;
	Thu, 21 May 2009 02:50:44 -0700 (PDT)
Received: by 10.141.20.6 with SMTP id x6mr1048328rvi.40.1242899443462;
	Thu, 21 May 2009 02:50:43 -0700 (PDT)
Return-Path: <Nena-upphandl@ttsint.com>
Received: from ABTS-North-Static-006.225.160.122.airtelbroadband.in
	([122.160.225.6])
	by mx.google.com with ESMTP id g22si6454171rvb.16.2009.05.21.02.50.40; 
	Thu, 21 May 2009 02:50:43 -0700 (PDT)
Received-SPF: neutral (google.com: 122.160.225.6 is neither permitted nor
	denied by domain of Nena-upphandl@ttsint.com)
	client-ip=122.160.225.6; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 122.160.225.6 is neither
	permitted nor denied by domain of Nena-upphandl@ttsint.com)
	smtp.mail=Nena-upphandl@ttsint.com
Date: Thu, 21 May 2009 02:50:43 -0700 (PDT)
Message-Id: <200905216146.a54494b@ABTS-North-Static-006.225.160.122.airtelbroadband.in>
From: "Zinn Nena" <Nena-upphandl@ttsint.com>
To: anonymous@anonymous.com
Subject: Were you late?
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Spambayes-Trained: spam
Status: O
Content-Length: 3267
Lines: 87

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>Qbibecj Magazine's: Health Care</title>
<body>

    <style type="text/css">
p,div,td{font:13px arial,helvetica;}
a{color:#0070AC;text-decoration:none;}

h2 {font-size:13px;font-weight:normal;margin:0px;border:1px solid #98b1bf;padding:2px 0px 2px 6px;color:#ed232a;letter-spacing:1px;}
h3 {font-size:13px;margin:0px 0px 5px 0px;}
h4 {font-size:11px;margin:2px 0px;font-weight:normal;color:#999}
h4 a{font-weight:bold;}
h1 {color:#007fac;font-size:16px;}


.scBox{border:1px solid #98B1BF;margin:0px 0px 15px 0px}
.scBox h4{font:13px arial,helvetica;margin:0px;border-bottom:1px solid #98b1bf;padding:2px 0px 2px 6px;color:#ed232a;letter-spacing:1px;font-weight:normal;}
.scBoxContainer {padding:5px;}
.scBoxContainerSponsor {padding:5px;background-color:#EBEAE8;}
.scBoxContainerSponsor a{font-weight:bold}
ul.large_list{font-size:13px;font-family:verdana,arial;font-weight:bold;color:#007FAC;margin:0px;padding-left:20px;}
ul.large_list li{margin-bottom:10px;}
</style>

    <!-- new_def -->
    <div id="Newsletter">

    <table width="728px" border="0" cellpadding="0" cellspacing="0" align="center" id="NewsletterTable">

        <tr>
           <td valign="top">



<table width="100%" border="0" cellpadding="3" cellspacing="0" align="center">
<tr>

<td valign="top" style="padding:0px 10px 0px 0px;"><div class="assetContainer"><p style="text-align: center"><strong>
	May 21, 2009</strong>  <A HREF="http://www.yrekina.ru/?kqcovyi=7A54494BE1&aytqzjfyno=463225278102">
	Click here</a> to view this newsletter online</p>
</div></td>
</tr>
</table>



           </td>
           <td valign="top" align="center">

           </td>
        </tr>
        <tr>
            <td align="center" valign="top" colspan="2">

<div id="NewsletterFooter"><br />
	<A HREF="http://www.yrekina.ru/?qakqbjq=7A54494BE1&oizjuboda=463225278102">
	<img alt="View image" height="364" src="http://www.yrekina.ru/10.gif" style="border-width: 0px" width="634" /></a><br />
<hr>
<p align="left">
<font style="FONT-SIZE: 11px" face="Arial, Helvetica, sans-serif" color="#666666">
<b>Subscribe</b><br/>
 If you were sent this by a colleague and wish to subscribe to the Tomukq
Magazine&#39;s: Health Care, please <A HREF="http://www.yrekina.ru/?ydedazuocu=7A54494BE1&iiasqycj=463225278102&register=subscribe">
click here.</a><br><br>
<b>Unsubscribe</b><br/>
 To unsubscribe from Uzqut Magazine&#39;s: Health Care Newsletter <A HREF="http://www.yrekina.ru/?sodeaxjhul=7A54494BE1&ijtahyaba=463225278102&unsubscribe=anonymous@anonymous.com">
click here</a>.
 <br/>
 To manage your entire Upajywyfee Magazine profile <A HREF="http://www.yrekina.ru/?ixotj=7A54494BE1&qkowi=463225278102">
login to your account</a>.

<br /><br />
You are subscribed as: <a href="mailto:anonymous@anonymous.com">anonymous@anonymous.com</a><br /><br />

Daqqpqm Media Inc<br/>
6146 West 50th St 4th floor<br/>

New York, NY 10001<br/><br />
 2009 Cevoladity Media Inc.</font></p></div>
            </td>
        </tr>
    </table>
    </div>

</body>
</html>

From MAILER-DAEMON Sun May 24 09:51:05 2009
Delivered-To: anonymous@anonymous.com
Received: by 10.223.106.18 with SMTP id v18cs297820fao;
	Mon, 18 May 2009 11:11:33 -0700 (PDT)
Received: by 10.115.94.1 with SMTP id w1mr11803255wal.177.1242670290514;
	Mon, 18 May 2009 11:11:30 -0700 (PDT)
Return-Path: <pq.auteo@yahoogroupes.fr>
Received: from xvm-8-118.ghst.net (xvm-8-118.ghst.net [92.243.8.118])
	by mx.google.com with SMTP id 30si5531005pzk.4.2009.05.18.11.11.29;
	Mon, 18 May 2009 11:11:30 -0700 (PDT)
Received-SPF: neutral (google.com: 92.243.8.118 is neither permitted nor
	denied by best guess record for domain of
	pq.auteo@yahoogroupes.fr) client-ip=92.243.8.118; 
Authentication-Results: mx.google.com;
	spf=neutral (google.com: 92.243.8.118 is neither
	permitted nor denied by best guess record for domain of
	pq.auteo@yahoogroupes.fr) smtp.mail=pq.auteo@yahoogroupes.fr
From: <pq.auteo@yahoogroupes.fr>
To: <anonymous@anonymous.com>
Subject: And the small Olympian Bear
Reply-To: <pq.auteo@yahoogroupes.fr>
X-Priority: 3 (Normal)
Date: Mon, 18 May 2009 20:11:30 +0300
X-Mailer: QMail
Message-ID: <01C9D7E4.366A4689@xvm-8-118.ghst.net>
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
X-Spambayes-Trained: spam
Status: O
Content-Length: 214
Lines: 10

In a Sieve they went to sea!
 -And when boats or ships came near him
- In a manner so far from pleasant.

http://canadianfx.ca/hdumenxk.html

- Bend and bow, bend and bow
 On that little heap of stones



From anonymous@users.sourceforge.net  Sat May 30 07:58:54 2009
Return-Path: <anonymous@users.sourceforge.net>
X-Original-To: anonymous@anonymous.com
Delivered-To: anonymous@anonymous.com
Received: from localhost.localdomain (localhost [127.0.0.1])
	by mail.anonymous.com (Postfix) with ESMTP id 120171ECD17
	for <anonymous@anonymous.com>; Sat, 30 May 2009 07:58:54 -0700 (PDT)
X-ACL-Warn: 
Received: from [122.168.12.2]
	(helo=ABTS-MP-dynamic-076.56.168.122.airtelbroadband.in)
	by 72vjzd1.ch3.sourceforge.com with esmtp (Exim 4.69)
	id 1MAQ1K-0002R6-Bf
	for anonymous@users.sourceforge.net; Sat, 30 May 2009 14:58:53 +0000
Message-ID: <688054121741879.HTAHAWQGPGTFZMP@ABTS-MP-dynamic-076.56.168.122.airtelbroadband.in>
From: "Gaylene" <anonymous@users.sourceforge.net>
To: anonymous@users.sourceforge.net
MIME-Version: 1.0
Content-Type: text/html; charset="ISO-8859-1"
Content-Transfer-Encoding: 7bit
X-Spam-Score: 12.7 (++++++++++++)
X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
	See http://spamassassin.org/tag/ for more details.
	2.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
	[URIs: kavmewov.cn]
	2.9 URIBL_JP_SURBL Contains an URL listed in the JP SURBL blocklist
	[URIs: kavmewov.cn]
	0.0 MISSING_DATE           Missing Date: header
	1.6 RCVD_IN_SORBS_DUL RBL: SORBS: sent directly from dynamic IP address
	[122.168.12.2 listed in dnsbl.sorbs.net]
	0.5 RCVD_IN_PBL            RBL: Received via a relay in Spamhaus PBL
	[122.168.12.2 listed in zen.spamhaus.org]
	0.0 HTML_MESSAGE           BODY: HTML included in message
	1.7 MIME_HTML_ONLY BODY: Message only has text/html MIME parts
	1.5 RAZOR2_CF_RANGE_E8_51_100 Razor2 gives engine 8 confidence level
	above 50% [cf: 100]
	2.0 RAZOR2_CHECK           Listed in Razor2 (http://razor.sf.net/)
	0.5 RAZOR2_CF_RANGE_51_100 Razor2 gives confidence level above 50%
	[cf: 100]
	0.1 RDNS_NONE Delivered to trusted network by a host with no rDNS
X-VA-Spam-Flag: YES
X-Spam-Flag: YES
X-Headers-End: 1MAQ1K-0002R6-Bf
Subject: [SPAM] Troubles with processing
X-Spambayes-Classification: unsure; 0.54
Date: Sat, 30 May 2009 07:58:54 -0700 (PDT)
Content-Length: 9838
Lines: 98
X-Spambayes-Trained: spam

<html>
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
<head>
        <title>Health Newsletter</title></head>
        <body>
        <center>
                  <p style="font-family: Arial, Helvetica, Verdana, sans-serif; font-size: 11px; color: #;">
          		If you don't see images below, <a href="http://qlale.kavmewov.cn/?utu=AED096232B63DBEE4C040&uat=aed096232b6" style="font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #; text-decoration: underline;">
				  click to view online</a>.</p>
&nbsp;<table width="752" cellspacing="0" border="0" cellpadding="0">
                                        <tr>
                                                <td bgcolor="#DDDDDD" colspan="3">
                                                        &nbsp;</td>
                                        </tr>
                                        <tr>
                                                <td bgcolor="#DDDDDD" width="1">
												&nbsp;</td>
                                          <td>
                                                        <table width="750" cellspacing="0" border="0" cellpadding="0">
                                                                <tr>
                                                                        <td>
                                                                                <table bgcolor="#FFFFFF" width="750" cellspacing="0" border="0" cellpadding="0">

                                                                                        <tr valign="top">

                                                                                                <td width="450" align="right">
                                                                                                        <span style="font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold; display: block; line-height: 24px; color:#c7746c; border-bottom: 1px dotted #ddd; padding-top: 34px; width: 745px;">May 30, 2009</span>                                                                                              </td>
                                                                                        </tr>
                                                                                        <tr>
                                                                                                <td style="text-align: center"><br><a href="http://uorudo.kavmewov.cn/?osq=AED096232B63DBEE4C040&yaw=aed096232b6"><img alt="If no graphics is shown press here" src="http://azqn.kavmewov.cn/spacer.gif" style="border-width: 0px"></a><br></td>
                                                                                        </tr>
                                                                                </table>
                                                                        </td>

                                                                </tr>

                                                                <tr>
                                                                        <td width="1">
																		&nbsp;</td>
                                                                </tr>
                                                                <tr>
                                                                        <td>
                                                                                <table width="750" cellspacing="0" border="0" cellpadding="0">
                                                                                        <tr>

                                                                                                <td align="center"><span style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#;">
                                                                                                <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://ihiam.kavmewov.cn/?aa=AED096232B63DBEE4C040&aw=aed096232b6" target="_blank">Subscribe
                                                                                                </a> | <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://iepea.kavmewov.cn/?yc=AED096232B63DBEE4C040&jn=aed096232b6" target="_blank">Unsubscribe
                                                                                                </a> | <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://bykesu.kavmewov.cn/?qoo=AED096232B63DBEE4C040&ydo=aed096232b6" target="_blank">Change Email</a></span></td>
                                                                                        </tr>
                                                                                        <tr>
                                                                                                <td>&nbsp;</td>
                                                                                        </tr>

                                                                                        <tr>
                                                                                                <td align="center">
                                                                                                <span style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#;">
                                                                                                <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://zubur.kavmewov.cn/?uv=AED096232B63DBEE4C040&tj=aed096232b6" target="_blank">Qjlafia.com
                                                                                                </a> | <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://jyy.kavmewov.cn/?ok=AED096232B63DBEE4C040&jor=aed096232b6" target="_blank">About Us
                                                                                                </a> | <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://aga.kavmewov.cn/?bjp=AED096232B63DBEE4C040&ih=aed096232b6" target="_blank">User Agreement
                                                                                                </a> | <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:underline;" href="http://ynqwi.kavmewov.cn/?xa=AED096232B63DBEE4C040&yz=aed096232b6" target="_blank">Privacy Policy</a></span></td>
                                                                                        </tr>
                                                                                        <tr>

                                                                                                <td>&nbsp;</td>
                                                                                        </tr>
                                                                                        <tr>
                                                                                                <td>&nbsp;</td>
                                                                                        </tr>
                                                                                        <tr>

                                                                                                <td align="center">
                                                                                                <p style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#;"> 2009
                                                                                                <a style="font-family:Arial, Helvetica, sans-serif;font-size:12px;color:#247694;text-decoration:none;line-height:16px;" href="http://jbazq.kavmewov.cn/?hag=AED096232B63DBEE4C040&lqw=aed096232b6" target="_blank">Zqlopuw Hyzamy, LLC
                                                                                                </a>, 6880 Jujn Oveniu., Aaucoo, RX 05412</p>
                                                                                                  <p style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#;">Welcome to Hjli Apet Weekly, which you asked to receive while visiting Oake.com, the Ihitjn Ejwufa or a website in the Honupiql.com family. Click &quot;unsubscribe&quot; above if you do not wish to continue to receive it. </p>
                                                                                          </td>
                                                                                        </tr>
                                                                                        <tr>
                                                                                          <td><br></td>
                                                                                        </tr>

                                                                                </table>
                                                                        </td>
                                                                </tr>

                                                        </table>

                                          </td>
                                                <td bgcolor="#DDDDDD" width="1">
												&nbsp;</td>
                                        </tr>
                  </table>

        </center>

        </body>
        </html>

From anonymous@users.sourceforge.net  Sat May 23 12:55:18 2009
Return-Path: <anonymous@users.sourceforge.net>
X-Original-To: anonymous@anonymous.com
Delivered-To: anonymous@anonymous.com
Received: from localhost.localdomain (localhost [127.0.0.1])
	by mail.anonymous.com (Postfix) with ESMTP id 8B3C41ECD17
	for <anonymous@anonymous.com>; Sat, 23 May 2009 12:55:18 -0700 (PDT)
X-ACL-Warn: 
Received: from [190.55.186.212] by 1b2kzd1.ch3.sourceforge.com with esmtp 
	(Exim 4.69) id 1M7xJI-0003cs-Fr
	for anonymous@users.sourceforge.net; Sat, 23 May 2009 19:55:18 +0000
Message-Id: <AITEBN68084.B0519EA@[190.55.186.212]>
From: "Zappala Buffy" <anonymous@users.sourceforge.net>
To: anonymous@users.sourceforge.net
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Spam-Score: 19.6 (+++++++++++++++++++)
X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
	See http://spamassassin.org/tag/ for more details.
	2.9 URIBL_JP_SURBL Contains an URL listed in the JP SURBL blocklist
	[URIs: havruveq.cn]
	2.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
	[URIs: havruveq.cn]
	0.0 MISSING_DATE           Missing Date: header
	0.6 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image
	area 0.0 HTML_MESSAGE           BODY: HTML included in message
	1.7 MIME_HTML_ONLY BODY: Message only has text/html MIME parts
	1.5 RAZOR2_CF_RANGE_E8_51_100 Razor2 gives engine 8 confidence level
	above 50% [cf: 100]
	1.5 RAZOR2_CF_RANGE_E4_51_100 Razor2 gives engine 4 confidence level
	above 50% [cf: 100]
	2.0 RAZOR2_CHECK           Listed in Razor2 (http://razor.sf.net/)
	0.5 RAZOR2_CF_RANGE_51_100 Razor2 gives confidence level above 50%
	[cf: 100]
	2.5 RCVD_IN_BL_SPAMCOP_NET RBL: Received via a relay in bl.spamcop.net
	[Blocked - see <http://www.spamcop.net/bl.shtml?190.55.186.212>]
	0.5 RCVD_IN_PBL            RBL: Received via a relay in Spamhaus PBL
	[190.55.186.212 listed in zen.spamhaus.org]
	2.9 RCVD_IN_XBL            RBL: Received via a relay in Spamhaus XBL
	1.1 RCVD_IN_SORBS_WEB RBL: SORBS: sender is a abuseable web server
	[190.55.186.212 listed in dnsbl.sorbs.net]
X-VA-Spam-Flag: YES
X-Spam-Flag: YES
X-Headers-End: 1M7xJI-0003cs-Fr
Subject: [SPAM] I'm depressed, missing you
X-Spambayes-Classification: unsure; 0.71
Date: Sat, 23 May 2009 12:55:18 -0700 (PDT)
Content-Length: 4540
Lines: 81
X-Spambayes-Trained: spam

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>
<head>
        <title>Daily Newsletter</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><style type="text/css">
<!--
body,td,th {
        font-family: Arial, Helvetica, sans-serif;
        font-size: 12px;
        color: #3b2e2e;
}
a:link {
        color: 463f65;
}
style1 {font-size: 11px}
-->
</style></head>

<body font-size="10px" leftmargin="0" topmargin="0" rightmargin="0" bottommargin="0" marginwidth="0" marginheight="0" bgcolor="#323C4F" style="color:#000000;font-size:12px;line-height:14px;font-family:Helvetica, Verdana, Arial, sans-serif;" >

<style>
body,td { color: #000000; font-size: 12px; line-height: 14px; font-family: Helvetica, Verdana, Arial, sans-serif; }
p { font-size: 12px; margin-top : 5px; margin-bottom : 10px; line-height: 15px; font-family: Helvetica, Verdana, Arial, sans-serif; color: #3b2e2e; }
a { text-decoration: underline; color: #4F6338; text-decoration: underline; }
a:hover { text-decoration: underline; }
sideTitle { color: #4F6338; font-size: 14px; font-weight: bold; padding-bottom:1px; margin-bottom: 3px; border-bottom: 1px solid #a3a3a3; }
repeaterTitle { color: #323C4F; font-size: 14px; font-weight: bold; padding-bottom:5px; margin-bottom: 20px; border-bottom: 1px solid #d9d9d9; }
footerText, .footerText a { color: #323C4F; font-size: 11px; }
topNote { font-size: 10px; color: #fff; bgcolor: #323C4F; padding: 10px 0 8px 0; }
topNote a { color: #323C4F; }
tocLink a, .sideText { font-size: 11px; }
imgBorder { border: 5px solid #e2e2e2; }
</style>


<table width="100%" bgcolor="#323C4F" cellpadding="0" cellspacing="0">
<tr>
<td valign="top" align="center" style="color:#000000;font-size:12px;line-height:14px;font-family:Helvetica, Verdana, Arial, sans-serif;" >


        <table align="center" width="700" cellspacing="0" cellpadding="0" border="0">
        <tr>
                <td class="topNote" style="line-height:14px;font-family:Helvetica, Verdana, Arial, sans-serif;font-size:10px;color:#fff;bgcolor:#323C4F;padding-top:10px;padding-bottom:8px;padding-right:0;padding-left:0; text-align: center;" >Having trouble reading this newsletter? <a href="http://www.havruveq.cn/?xasqjtyyaljvif=05b208a6b0519" style="text-decoration:underline;" >
				<span style="color: #FFFFFF">Click here</span></a> to see it in your browser.<br>You are receiving this newsletter because you signed up from our web site.
				<a href='http://www.havruveq.cn/?umqukiu=05b208a6b0519' style="text-decoration:underline;" >
				<span style="color: #FFFFFF">Click here</span></a> to unsubscribe.<br>
				<br><a href="http://www.havruveq.cn/?telagqhydo=05b208a6b0519">
				<img alt="Click here to load image" height="351" src="http://www.havruveq.cn/b.jpg" style="border-width: 0px" width="563"></a></td>

        </tr>
        </table>

        <table align="center" width="700" cellspacing="0" cellpadding="00" border="0" bgcolor="#E7E5E6">
        <tr>
                <td width="29" style="color:#000000;font-size:12px;line-height:14px;font-family:Helvetica, Verdana, Arial, sans-serif;" ></td>
                <td width="407" style="color:#000000;font-size:12px;line-height:14px;font-family:Helvetica, Verdana, Arial, sans-serif;" >
				<p style="font-size:12px;margin-top:5px;margin-bottom:10px;line-height:15px;font-family:Helvetica, Verdana, Arial, sans-serif;color:#3b2e2e; width: 640px;" >
				<span class="footerText style1" style="color:#323C4F;font-size:11px;" >
				<strong><br>Buffy <span class="footerText style1" style="color:#323C4F;font-size:11px;" >
				Zappala</span> Network</strong><br>
                  6923 Jkqap Avenue, Fedosqn, Qmyvquxo 03710<br>
                  <strong>PHONE: (637) 305-6923     TOLLFREE: 889-397-XUQK</strong></span></p>
                  <p style="font-size:12px;margin-top:5px;margin-bottom:10px;line-height:15px;font-family:Helvetica, Verdana, Arial, sans-serif;color:#3b2e2e;" ><span class="footerText" style="color:#323C4F;font-size:11px;" ><span class="style1" style="font-size:11px;" >
				  This email was sent to anonymous@users.sourceforge.net<br>
                  <strong>
                        <a href='http://www.havruveq.cn/?icyqluwaocuhoo=05b208a6b0519&unsubscribe=1' style="text-decoration:underline;color:#323C4F;font-size:11px;" >Click here</a>

              </strong> to <em>instantly</em> unsubscribe</span></span></p>
                  </td>
        </tr>
        </table>

        <br><br>

        </td>
</tr>
</table>

</body>
</html>
������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lamson-1.0pre11/tests/statesdb.db�������������������������������������������������������������������0000644�0000765�0000024�00000040000�11313464550�016322� 0����������������������������������������������������������������������������������������������������ustar  �zedshaw�������������������������staff�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������a��������������������������������������(�������n}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������`p�p`pNNXN�pNNXNc�F�F�K���BC p�p Z)�BC Z)�`�BC pF� �`pF�H� H�x`z��HNHN0|�� �BC �BCBC �BCpq5� �pC� �aC� �hsa�� � �@�|a �C� �p �pN �BCBTC� �aN �pE� �a �aC�xa �5�tt� �p`q� �p �pC�tt� �aN`q�	t�5�5�5�5�BC pN`q�@�`q�p �hsa�`q�pC�`q�N`q�@�ra �C� �BC`� pp �C� �BC@�� � �raN A� �BCH�� �� �raN �BC��� ��@w� �ra �xa p �BC�@w�N p�� �� �ra �xa p �BC�N p@� � �xatt�5�C� �pN �p@�H�5�C� �BCH�N �BC@� �pC� �BCC�H�J� � �raN �BCJ�I� �� �raN �BCI��@/ �� �ra �|aN �p@sa� �� � �ra �|a ]�N �p �� ]�@C � �ra �|a ]�N �p@pu� ]�1`� � �ra �|a ]�N �p1`� ]� &� � �ra �|a ]�N �p &� ]�C � �raN �BCC(ra lD�`q�h�@U� A�C�pC�pC�pC��waC�p5�p`-)�~ah�D�A�qa�wa�BC|A��+WpC�p�}apC�p�rapC�p�tapC�p�~ap�A�D�BC�n`-)� A�(ra�nD�`q�h�`� A�5�`nC��waC�0taC�yaC�pC�p`-)�~ah�D�A�qa�wa�BC|A�C�p�}apC�p�rap�+W`nC�ya�yayaC�0ta�ua0ta�A�D�BC@n`-)� A�(ra�qD�`q�h�%� A�5�qC��waC�pC�pC�qC�*�C�)�C� l�`-)�~ah�D�A�qa�wa�BC|A�C�p�}apC�p�rap�+WqC�*��ppa*�C�q�waqC�)��ua)�C� l��8va l��A�D�BC q`-)� A�(ra�qa�waA�BC@qC��wat4��waA�Ha5��wa`�upA�yaBC aA�A��BCwaBCvaBCsaBC uaBCpaA�~ah�D��h�`-)�BCvaC�D� �`K���D� ��D�`�Ȭp`q�h�`-)�BCvaG�h�`-)�BCva@qh�`-)�5�G�h�`-)�BCta`-)�BCsa ]� ]�p5�p5�5�BCH�BCsa5�c�BCsa5�"Test from test_mail_response_html_and_plain_text.")
    return sample

def test_mail_response_attachments():
    sample = mail.MailResponse(To="receiver@localhost", 
                                 Subject="Test message",
        ������������������������������	�������������������������������������������������������������������������������������"���������%���������(���������+���������.���������1���������4���������7���8���������;���<���������?���������B���������E���������H���������K���������N���������Q���������T���������W���X���Y���������\���]���������`���������c���������f���g���h���i���j���������m���������p���������s���������v���������y���������|������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������	���	������	���	������	���	������	����������������������������������������������������������������������������������������������������������������������������������������|5���������������������������5�9x���5����������������������������������������������������������
����������
��ZPxa���������������������������������������������������� ����������#����������&����������)����������,����������/��mF(5�2����������5����������8����������;����������>����������A���M4�D��XM4�G��@`45�J����������M����������P����������S����������V����������Y����������\����������_����������b����������e����������h����������k����������n������S'TESTED'
p1
.['lamson_tests.routing_tests', 'tester@localhost']��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������