2010-03-05

This blog has moved


This blog is now located at http://mykzilla.blogspot.com/.
You will be automatically redirected in 30 seconds, or you may click here.

For feed subscribers, please update your feed subscriptions to
http://mykzilla.blogspot.com/feeds/posts/default.

2009-11-17

The Skinny on Raindrop's Mailing List Extensions

Raindrop is an exploration of messaging innovation that strives to intelligently assist people in managing their flood of incoming messages. And mailing lists are a common source of messages you need to manage. So, with assistance from the Raindrop hackers, I wrote extensions that make it easier to deal with messages from mailing lists.

Their goal is to soothe two particular pain points when dealing with mailing lists: grouping their messages together by list and unsubscribing from them once you're no longer interested in their subject matter.

This post explains how the extensions do this; touches on some aspects of Raindrop's message processing and data storage models; and speculates about possible future directions for the extensions.

Raindrop Extensibility

Raindrop is being built with the explicit goal of being broadly and deeply extensible, and it includes a number of APIs for adding and modifying functionality. The mailing list enhancements comprise two related extensions, one in the backend and one in the user interface.

The backend extension plugs into Raindrop's incoming message processor, intercepting incoming email messages and extracting info about the mailing lists to which they belong. It also handles much of the work of unsubscribing from a list.

The frontend extension plugs into Raindrop's Inflow application, modifying its interface to show you the most recent mailing list messages at a glance, group mailing list conversations together by list, and provide a button you can press to easily unsubscribe from a mailing list.

Message Processing and Data Storage

Before getting into how the extensions work, it's useful to know a bit about how Raindrop processes and stores messages.

Raindrop stores information using CouchDB, a document-centric database whose principal unit of information storage and retrieval is the document (the equivalent of a record in SQL databases). Documents are just JSON blobs that can contain arbitrary name -> value pairs (unlike SQL records, which can only contain values for predeclared columns).

To distinguish between different kinds of documents, Raindrop assigns each a schema (similar to a table in SQL parlance) that describes (and may one day constrain) its properties. The rd.msg.email schema is the primary schema representing an email message, while the rd.mailing-list is the schema representing a mailing list, and the rd.msg.email.mailing-list is a simple schema that associates messages with their lists.

(In an SQL database, rd.msg.email and rd.mailing-list would be tables whose rows represent email messages and mailing lists, while rd.msg.email.mailing-list would be a table whose rows map one to the other.)

Note that there's a many-to-one relationship between messages and lists, since messages belong to a single list, although lists contain many messages, so rd.msg.email.mailing-list isn't strictly necessary. Its list-id property (which identifies the list to which the message belongs) could simply be a property of rd.msg.email docs (or, in SQL terms, a foreign key in the rd.msg.email table).

But putting it into its own document has several advantages. First, it improves robustness, as it reduces the possibility of conflicts between extensions and core code writing to the same documents.

It also improves write performance, as it's faster to add a document than to modify an existing one (although index generation and read performance can be an issue).

Finally, it improves extensibility, because it makes it possible to write an extension that extends the backend mailing list extension.

That's because Raindrop's incoming message processing model allows extensions to observe the creation of any kind of document, including those created by other extensions.

So just as the mailing list extension observes the creation of rd.msg.email documents, another extension can observe the creation of rd.msg.email.mailing-list documents and process them further in some useful way. If the mailing list extension simply modified the original document instead of creating its own, that would require some additional and more complicated API.

The Backend Extension

The primary function of the backend extension is to examine every incoming message and dress the ones from mailing lists with some additional structured information that the frontend can use to organize them.

Backend extensions are accompanied by a JSON manifest that tells Raindrop what kinds of incoming documents it wants to intercept. The mailing list extension's manifest registers it as an observer of incoming rd.msg.email documents, which get created when Raindrop retrieves an email message:
"schemas" : {
"rd.ext.workqueue" : {
"source_schemas" : ["rd.msg.email"],
...

The extension itself is a Python script with a handler function that gets passed the rd.msg.email document and looks to see if it contains a List-ID header (or, in certain cases, another identifier) identifying the mailing list from which the message comes:
def handler(message):
...
if 'list-id' in message['headers']:
# Extract the ID and name of the mailing list from the list-id header.
# Some mailing lists give only the ID, but others (Google Groups,
# Mailman) provide both using the format 'NAME <id>', so we extract them
# separately if we detect that format.
list_id = message['headers']['list-id'][0]
...

If it doesn't find a list identifier, it simply returns, and Raindrop continues processing the message:
if not list_id:
logger.debug("NO LIST ID; ignoring message %s", message_id)
return

Otherwise, it calls Raindrop's emit_schema function to create an rd.msg.email.mailing-list document linking the message document to an rd.mailing-list document representing the mailing list:
emit_schema('rd.msg.email.mailing-list', { 'list_id': list_id })

In this function call, rd.msg.email.mailing-list is the type of document to create, while { 'list_id': list_id } is the document itself, written as Python that will get serialized to JSON.

A document created inside a backend extension like this automatically gets a reference to the document the extension is processing (i.e. the rd.msg.email document), so the only thing it has to explicitly include is a reference to the list document, in the form of a list_id property whose value is the list identifier.

The extension also checks if there's an rd.mailing-list document in the database for the mailing list itself, and if not, it creates one, populating it with information from the message's List-* headers, like how to unsubscribe from the list. Otherwise, it updates the existing mailing list document if the message's List-* headers contain updates.

The Frontend Extension

The frontend extension uses the information extracted by the backend to help users manage mailing lists in the Inflow application.

It adds a widget to the Home view that shows you the last few messages from your lists at the bottom of the page, so you can keep an eye on those messages without having to give them your full attention:




It adds a list of your mailing lists to the Organizer widget:




And when you click on the name of a list, it shows you its conversations in the conversation pane:




In traditional mail clients, users who want to break out their list messages into separate buckets like this typically have to create a folder for each list to contain its messages and then a filter for each list to move incoming list messages into the appropriate folders. The extension does this for you automatically!

Finally, while viewing list conversations, if the extension knows how to unsubscribe you from the list, it displays an Unsubscribe button:




Pressing the button (and then confirming your decision) unsubscribes you from the list. You don't have to do anything else, like remembering your username/password for some web page, sending an email, or confirming your request with the list admin. The extensions handle all those details for you so you don't have to know about them!

List Unsubscription

In case you do want to know the details, however, it goes like this...

First, the frontend extension sends a message to the list's admin address requesting unsubscription, with a certain command (like "unsubscribe") in the subject or body of the message (lists often specify exactly what command to send in the mailto: link they include in the List-Unsubscribe header):
From: Jan Reilly 
To: wasbigtalk-admin@example.com
Subject: unsubscribe

Then the server responds with a message requesting confirmation of the request, often putting a unique token into the Subject or Reply-To header to track the request:
From: wasbigtalk-admin@example.com
To: jan@example.com
Subject: please confirm unsubscribe from wasbigtalk (4bc3b7e439fd)

Hello jan@example.com,

We have received a request to unsubscribe you from wasbigtalk.
Please confirm this request to unsubscribe by replying to this email.
...

Then the backend extension responds with a message confirming the request that includes the unique token:
From: jan@example.com
To: wasbigtalk-admin@example.com
Subject: Re: please confirm unsubscribe from wasbigtalk (4bc3b7e439fd)

Finally, the server responds with a message confirming that the subscriber has, indeed, been unsubscribed:
From: wasbigtalk-admin@example.com
To: jan@example.com
Subject: you have been unsubscribed from wasbigtalk

Hello jan@example.com,

Your unsubscription from wasbigtalk was successful.
...

At this point, the backend extension marks the list unsubscribed in the database, and the frontend extension marks it unsubscribed in the user interface.

This process matches the way much mailing list server software works, although there are daemons in the details, so the extensions have to be programmed to support each server individually.

Currently, they know how to handle Google Groups and Mailman lists. Majordomo2 (used by the Bugzilla and OpenBSD projects, among others) is not supported, because it doesn't send List-* headers (alhough supposedly it can be configured to do so). The W3C's list server is not yet supported, although it does send List-* headers, and support should be fairly easy to add.

Note that some of the processing the extension does is (locale-dependent) "screen"-scraping, as Google Groups and Mailman don't consistently identify the list ID and message type in some of their correspondence. In the long run, hopefully server software will improve in that regard. Perhaps someone can spearhead an effort to make it so?

The Future

The extensions' current features fit in well with Raindrop's goal of helping people better handle their flood of incoming messages. But there is surely much more they could do to help in this regard.

Besides general improvements to reliability and robustness--like support for additional list servers and handling of localized admin messages--they could let you resubscribe to a mailing list from which you've unsubscribed. And perhaps they could automatically fetch the messages you missed while you were away. Or even retrieve the entire archive of a list to which you're subscribed, so you can browse the archive in Raindrop!

What bugs you about mailing lists? And how might Raindrop's mailing list extensions make them easier (and even funner) to use?

2009-11-04

Building/Releasing Personas

Want to know how a popular extension like Personas gets built and released? Neither do I! Yet I know anyway. And I've written it down for your edification! So check it out.

2009-10-12

Makefile for building/distributing extensions

I've written a Makefile to help build and distribute Personas and Snowl. It automates the process of generating manifests; packaging the extension into a JAR and a XPI; and, for a "development" distribution channel, autogenerating successive version numbers and rsyncing the builds and update.rdf file to a distribution server.

The file makes it easy for me to update the latest development build of Personas after checking in a fix via:

make build channel=dev make package channel=dev make publish

For release builds, it's:

make build channel=rel make package channel=rel [manually upload to AMO]

And when I just want to test locally, I can build and install to a local profile:

make build make install profile=~/dev-profile firefox -profile ~/dev-profile

Or I can package the build and install via Firefox's Add-ons Manager:

make build make package [manually install via Firefox's Add-ons Manager]

Because I reuse it for multiple projects, project-specific variables are defined in a separate client.mk file. If you have similar needs and would find it useful, you are welcome to it!

Just grab the Makefile, see client.mk for the variables you should define, and check out the Personas source directory for the general layout that the Makefile handles as well as the install.rdf.in, chrome.manifest.in, and update.rdf.in templates from which their respective files are built.


Notes:
  1. It's originally based on Weave's top-level Makefile, although it doesn't currently support the binary components that Weave's Makefile builds.

  2. It assumes a directory structure in which content/, locale/, and skin/ directories are at the top level of the source directory (not within a chrome/ subdirectory).

  3. It currently builds in the source directory rather than in an object directory, which means it can leave old install.rdf, chrome.manifest, and chrome.jar files around, so sometimes I need to make clean && make build to get rid of the ones I previously built (f.e. for a release build whose chrome files are archived) and generate new ones for continued development.

  4. For Personas, I use a defaults/preferences/prefs.js.in template to generate prefs that contain the Mercurial revision ID (so I can identify the version of code in a development build with which someone is reporting problems) and the distribution channel (which the extension uses to suppress the "You've been updated!" page for the "dev" channel, since that would get annoying fast).

2009-08-29

Personas for Email Messages

Here's a thought (I had in the middle of a dream about brightly garbed superheroes): use Personas to style email messages in Thunderbird! It'd be a simple way to easily add some style to your messages to friends and family, highlighting your current mood, your perennial interests, or the subject of the message.

A dramatization mockup:



There are some tricky issues, like how to select a background color for the message that blends in attractively with the bottom of the header image. And it's not clear if the footer image is useful in this case.

But overall the idea seems quite promising. What do you think? And, if you're a Mozilla extension hacker (or would like to become one), would you be interested in prototyping it?

2009-04-23

The Problems With Pagination

Many web sites and applications paginate their content. Often this makes them harder to use.



Partly it's because site-provided pagination interacts poorly with the browser's built-in scrolling, because the content on any given web page usually takes up more than one vertical screen of space, and you have to jump back and forth between site pagination and browser scrolling, hunting down their controls each time you do so.

Sites compound this problem by moving their pagination controls around, so you can't leave your mouse over the Next Page button and assume you can simply click the mouse button repeatedly to jump to subsequent pages.

And pagination control click targets are small, which makes them hard to hit. Page-specific targets are often a single number (1, 2, 3), while the Next Page target is sometimes a single character (>).



Finally, pagination confounds searching through content using a browser's Find in Page feature, since it requires you to switch back and forth between searching for the content on the current page and navigating to the next one until you find the text you're looking for (and beyond, if you're looking for multiple occurrences of it).

And it similarly confounds any other activity in which you want to engage with all the content in an application, like when I wanted to turn off tracebacks/pingbacks for all 56 posts in a Wordpress blog, which is a six step process, and I had to repeat those six steps four times each because Wordpress's list of posts only lets me see 15 at a time.



Nevertheless, there are legitimate uses for pagination:
  • sites whose content is computationally intensive to generate (like search engines) use it to reduce load and cost;

  • those with large quantities of content that strains server, client, and network resources to display all at once (search engines again, the Personas gallery) use it to improve responsiveness;

  • those whose content cannot usefully be browsed in its entirety much of the time (search engines a third time, messaging applications) use it to reduce the cognitive overload of providing all their content at once;

  • and those with an ad-based revenue model (you guessed it, search engines, but also online news and many other sites) use it to show more ads.


For those kinds of sites, pagination makes sense, although doing it right still matters, like giving its controls large click targets and putting them in the same place on every page.

And, for content that spills across multiple screens, including the pagination controls at both the top and bottom of the content, the two scroll positions at which users often find themselves when they want to change pages; or positioning them absolutely, so they're always visible; or only loading a screenful of content at a time, so users never have to both page and scroll; or using endless/infinite scrolling.

Are there even better approaches, and are there ways for the web platform to support them so that web sites don't have to roll their own? Perhaps browsers and servers could collaborate to provide content in screenfuls with integrated scrolling and paging controls that play well together and don't change from site to site.

What do you think?

2009-04-16

syntaxes for iterating arrays in JavaScript

JavaScript lets you iterate arrays using the for each...in statement:
for each (var item in [1, 2, 3]) alert(item);

JavaScript 1.6 added the Array.forEach method:
[1, 2, 3].forEach(function(item) { alert(item) });

I've always preferred Perl's statement modifiers, though, for the popular English-like order of their clauses ("do this for each of those"):
print $_ foreach (1, 2, 3);

JavaScript 1.7 added array comprehensions for array initialization:
var squares = [item * item for each (item in [1, 2, 3])];

I just realized I can (ab)use comprehensions to iterate arrays with Perl-like syntax by throwing away the result:
[alert(item) for each (item in [1, 2, 3])];

I can iterate object properties the same way:
var obj = { foo: 1, bar: 2, baz: 3 };
[alert(name + "=" + obj[name]) for (name in obj)];

Sweet!

2009-04-09

Snowl Integration with Places

alta88 has recently done a bunch of work to integrate Snowl with Places. His initial efforts have focused on getting sources and people into Places, as he describes in this post to the discussion forum.

This will make the list of collections in the next version of Snowl work like Firefox's list of bookmarks in the Bookmarks Sidebar, and sources/people will also show up in the AwesomeBar:



Snowl doesn't yet register the snowl: protocol, however, so selecting those items doesn't do anything (yet). What do you think it should do?

And what additional Places-backed features would be useful?

(Picking up on something dietrich said on Twitter last week, I'd love to see Places get populated with tweets that reference a URL, setting the title of the Places entry to the content of the tweet, so you can use the AwesomeBar to find that site you saw last week in a tweet, as other folks have done with Delicious.)

2009-04-07

Snowl development builds

I've created a distribution channel to get Snowl changes out to its testing community faster. I'll push development snapshots to the channel on a regular basis as significant fixes are committed (or perhaps nightly, if I can automate the process).

Once you install a development build, Firefox will automatically check for newer ones and prompt you to update when it finds one, just as it does for the release builds on AMO.

This is truly the slicing edge. If you use these builds, be prepared for the possibility of regressions and bustage. On the other hand, you'll also get to test new features earlier, like alta88's Places integration work.

Install the latest development build, then discuss it and report bugs you find. (If you don't want to be on the slicing edge, try the latest stabler release instead.)

ISO simple open web image editor

Is there a simple open web image editor? I know about Pixlr, Picnik, and Aviary, but those all use Flash. It seems like it should be possible to make one using Canvas and other modern open web technologies.

In addition to using it for simple photo manipulations, I'd like one that can be embedded into other applications, like Personas (to make it easy for users to make and refine their own personas) and image hosting sites (so users don't have to go to a separate website and import their images to edit them).

In other words, I want the web-based image editing equivalent of Gecko+Firefox: a great open-source image rendering and editing engine with a flagship product that demonstrates its capabilities and an API for embedding it into other applications.

Is there such a thing? Does anyone want to create one?