diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -2,12 +2,12 @@
 
 This project is an email client for [notmuch](http://notmuchmail.org) written in
 [Haskell](http://www.haskell.org) and [Yesod](http://www.yesodweb.com).  The code implements a web
-server and uses [bootstrap](http://twitter.github.com/bootstrap/) and [jquery](http://jquery.com/)
+server and uses [bootstrap](http://twbs.github.io/bootstrap/) and [jquery](http://jquery.com/)
 for the UI.  The current features are
 
-* Search messages -
-  [search screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/search.png) and
-  [pager screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/pager.png)
+* Search messages - [search
+  screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/search.png) and [pager
+  screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/pager.png)
     * link default searches from the navigation bar
     * view results in a table, with customizable buttons for retagging each thread
     * view results in a pager, which shows the thread content together with a navigation bar with
@@ -17,17 +17,27 @@
     * a visible tree structure and the ability to collapse individual messages
     * download attachments from messages
     * customizable tagging buttons on each message
+    * On small screens (under 700 pixels width like phones), parse text messages as markdown and
+      display the original text message together with the resulting markdown HTML.  This allows the
+      message to be flowed to fit the small screen, but still allows one to view the original
+      message.  On larger screens, the original message is displayed by default but the markdown
+      HTML is available in a tab.
 * Compose email
     * sending with attachments
     * supports reply and reply all
-    * for compose, I recommend [Its All
+    * for composing messages, I recommend [Its All
       Text](https://addons.mozilla.org/en-us/firefox/addon/its-all-text/) to use your favorite
       editor.
+    * An optional address book.  Addresses can either be loaded from
+      [abook](http://abook.sourceforge.net/) on the server (abook must be in the path) or from
+      Google Contacts (loading from Google contacts requires a setting change in settings.yml).
 * Execute a raw notmuch command and view the results
 
+[ChangeLog](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/ChangeLog)
+
 # Quickstart
 
-To quickly test out the client on linux, make sure you have libgmp, zlib, and libicu installed,
+To quickly test out the client on linux, make sure you have libgmp and zlib installed,
 [download the latest binary](https://bitbucket.org/wuzzeb/notmuch-web/downloads), extract the
 tarball, then run
 
@@ -45,7 +55,12 @@
 One nice feature of GHC (the Haskell compiler) is the ability to statically link binaries to not
 require Haskell to be installed.  I have therefore built the latest release; you can find the
 tarballs on the [download page](https://bitbucket.org/wuzzeb/notmuch-web/downloads).  The only
-prerequisites are glibc, libgmp, libicu, and zlib.
+prerequisites are glibc, libgmp, and zlib.  The binaries are missing one feature: use of
+[libicu](http://site.icu-project.org/) to decode text/html message parts that have a charset that is
+not ISO-8859 or UTF-8 (UTF-8 and ISO-8859 are decoded internally without the help of libicu).  The
+reason for this restriction is that libicu is not generally binary compatible between different
+versions, so I could not link against a version of libicu that worked on many distributions.  I am
+working on removing this restriction so future versions should include all features.
 
 #### Source, latest released version
 
diff --git a/config/routes b/config/routes
--- a/config/routes
+++ b/config/routes
@@ -8,11 +8,11 @@
 /                               HomeR GET
 /search                         SearchPostR POST
 /search/#String                 SearchR GET
-/retag/thread/#ThreadID/#T.Text RetagThreadR POST
+/retag/thread/#ThreadID/#Text RetagThreadR POST
 /customretag/thread/#ThreadID   CustomRetagThreadR POST
 
 /thread/#ThreadID                 ThreadR GET
-/retag/message/#MessageID/#T.Text RetagMessageR POST
+/retag/message/#MessageID/#Text RetagMessageR POST
 /customretag/message/#MessageID   CustomRetagMessageR POST
 /messagepart/#MessageID/#Int      MessagePartR GET
 
@@ -23,3 +23,5 @@
 /compose             ComposeR GET POST
 /reply/#MessageID    ReplyR GET
 /replyall/#MessageID ReplyAllR GET
+/previewmsg          PreviewMessageR POST
+/abook/query         AbookQueryR GET
diff --git a/config/settings.yml b/config/settings.yml
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -1,7 +1,11 @@
 Default: &defaults
   host: "*4" # any IPv4 host
-  port: 3000
+  port: 3000 # can be overridden via the PORT environment variable or --port command line argument
 
+  # AGPLv3 requires a link to the source code on every page.  If you have not changed the
+  # source, you can leave it linked to my bitbucket page.
+  source-link: https://bitbucket.org/wuzzeb/notmuch-web/src
+
   # The folders are the notmuch searches that are linked from the navigation bar.
   # The first folder is displayed on the home page.
   folders:
@@ -13,7 +17,7 @@
         search: tag:lists and tag:unread
 
   # The retag buttons which appear for threads and messages.  The icon name is from
-  # the bootstrap glyphicons-halflings (http://twitter.github.com/bootstrap/base-css.html#icons).
+  # the bootstrap glyphicons-halflings (http://twbs.github.io/bootstrap/2.3.2/base-css.html#icons)
   retag:
       - name: Mark Read
         icon: icon-ok
@@ -34,13 +38,33 @@
   #message-id-domain: example.com
 
 
+  # Google Contact Support 
+  #
+  # The address fields (To, CC, BCC) on the compose form can optionally search
+  # contacts either from abook locally or can search from your google contacts.
+  # To enable google contacts, you must visit
+  # https://code.google.com/apis/console/ click on "API Access", click on
+  # "Create an OAuth Client ID", and fill in the data.  The application type
+  # must be "Web application" since the contacts will be loaded directly by
+  # javascript running in the browser.  The site/hostname must match where you
+  # serve notmuch-web from because of CORS protection
+  # (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing).  The Redirect
+  # URIs can be ignored since we will access google only via javascript. The
+  # Client ID can then be copied into the following setting.  Alternatively, an
+  # environment variable NOTMUCH_WEB_GOOGLE_CLIENTID will also be searched
+  # during startup for the client ID (useful for development).
+
+  #google-client-id: ""
+
+
 Production:
   # The approot is the full URL to root of notmuch web.
   approot: "http://localhost:3000"
 
-  # The from address for compose.  Currently only a single from address is allowed.  Format should be
-  #   Name <abc@example.com>
-  from-address: "x"
+  # List of one or more from addresses for compose.  Format should be  Name <abc@example.com>
+  from-addresses:
+      - "Name <abc@example.com>"
+      - "Name <abc@other.example.com>"
 
   # You must set your hashed password.
   # See http://hackage.haskell.org/packages/archive/pwstore-fast/latest/doc/html/Crypto-PasswordStore.html
@@ -59,7 +83,9 @@
 
 Development:
   approot: "http://localhost:3000"
-  from-address: "Test <test@example.com>"
+  from-addresses:
+      - "Test <test@example.com>"
+      - "Addr2 <addr2@example.com>"
   # For development and testing, the password is hunter2
   hashed-password: "sha256|12|lMzlNz0XK9eiPIYPY96QCQ==|1ZJ/R3qLEF0oCBVNtvNKLwZLpXPM7bLEy/Nc6QBxWro="
   sent-box: sent
@@ -67,7 +93,8 @@
 
 Testing:
   approot: "http://localhost:3000"
-  from-address: "Test <test@example.com>"
+  from-addresses:
+      - "Test <test@example.com>"
   # For development and testing, the password is hunter2
   hashed-password: "sha256|12|lMzlNz0XK9eiPIYPY96QCQ==|1ZJ/R3qLEF0oCBVNtvNKLwZLpXPM7bLEy/Nc6QBxWro="
   sent-box: sent
@@ -75,6 +102,7 @@
 
 Staging:
   approot: "http://localhost:3000"
-  from-address: "x"
+  from-address:
+      -  "Test <test@example.com>"
   hashed-password: "x"
   <<: *defaults
diff --git a/main.hs b/main.hs
--- a/main.hs
+++ b/main.hs
@@ -14,16 +14,21 @@
 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
+import Prelude hiding (putStrLn, getLine)
+
 import Application          (makeApplication)
-import Prelude              (IO, elem, Bool(..), ($), putStr)
+import Control.Monad        (when)
+import Crypto.PasswordStore (makePassword)
+import Data.Text.Encoding   (encodeUtf8, decodeUtf8)
+import Data.Text.IO         (getLine, putStrLn)
+import Data.Version         (Version(..))
+import NotmuchCmd           (notmuchVersion)
 import Settings             (parseExtra)
 import System.Environment   (getArgs)
-import System.IO            (hSetEcho, hFlush, stdin, stdout)
+import System.Exit          (exitWith, ExitCode(..))
+import System.IO            (hSetEcho, hFlush, stdin, stdout, hPutStrLn, stderr)
 import Yesod.Default.Config (fromArgs)
 import Yesod.Default.Main   (defaultMain)
-import Data.Text.IO         (getLine, putStrLn)
-import Data.Text.Encoding   (encodeUtf8, decodeUtf8)
-import Crypto.PasswordStore (makePassword)
 
 genPassword :: IO ()
 genPassword = do
@@ -38,6 +43,11 @@
 
 main :: IO ()
 main = do
+  v <- notmuchVersion
+  when (v < Version {versionBranch = [0,15], versionTags = []}) $ do
+      hPutStrLn stderr "notmuch-web requires at least notmuch 0.15"
+      exitWith $ ExitFailure 1
+
   a <- getArgs
   if "--make-password" `elem` a
     then genPassword
diff --git a/messages/en.msg b/messages/en.msg
--- a/messages/en.msg
+++ b/messages/en.msg
@@ -7,6 +7,7 @@
 Date: Date
 Num: Num
 Subject: Subject
+Body: Body
 Authors: Authors
 Attach: Attachments
 Tags: Tags
@@ -27,3 +28,12 @@
 Retag: Retag
 TagsToAdd: Tags to add
 TagsToRemove: Tags to remove
+BodyFormat: Interpret body as
+Preview: Preview Html
+AddrBookType: Address Book Type
+NoAddressBook: No address book
+Abook: Abook on server
+Google: Google Contacts
+GoogleAuth: Authorize Google Contacts
+AddrHelpTitle: Entering Email Addresses
+AddrHelp: Email addresses should either be entered directly or can be entered as "Name <email@example.com>" without the quotes.  To save the email address and start entering a new one, you can press comma.  Double clicking on an existing entry will allow you to edit it.
diff --git a/notmuch-web.cabal b/notmuch-web.cabal
--- a/notmuch-web.cabal
+++ b/notmuch-web.cabal
@@ -1,5 +1,5 @@
 name:              notmuch-web
-version:           0.1.1
+version:           0.1.2
 cabal-version:     >= 1.8
 build-type:        Simple
 synopsis:          A web interface to the notmuch email indexer
@@ -13,7 +13,8 @@
 description:       An email client for the notmuch email indexer (<http://notmuchmail.org>),
                    built using Yesod.  This project implements a web server and uses bootstrap
                    and jquery for the UI.  The client is fully functional, with searching, viewing,
-                   and composing email messages.
+                   and composing email messages.  See
+                   <https://bitbucket.org/wuzzeb/notmuch-web/src/tip/ChangeLog> for recent changes.
 
 data-files: README.md
             config/favicon.ico
@@ -46,6 +47,10 @@
     Description:   Build for use with "yesod devel"
     Default:       False
 
+Flag no-icu
+    Description:   Do not link against libicu
+    Default:       False
+
 library
     hs-source-dirs:  src
     exposed-modules: Application
@@ -55,12 +60,14 @@
                      StaticFiles
                      NotmuchCmd
                      FilterHtml
+                     Handler.Abook
+                     Handler.Compose
+                     Handler.ComposeFields
                      Handler.Home
-                     Handler.Tags
-                     Handler.View
                      Handler.Pager
                      Handler.Raw
-                     Handler.Compose
+                     Handler.Tags
+                     Handler.View
 
     if flag(dev) || flag(library-only)
         cpp-options:   -DDEVELOPMENT
@@ -68,6 +75,11 @@
     else
         ghc-options:   -Wall -O2
 
+    if flag(no-icu)
+        cpp-options:   -DNO_ICU
+    else
+        cpp-options:   -DUSE_ICU
+
     extensions: TemplateHaskell
                 QuasiQuotes
                 OverloadedStrings
@@ -81,60 +93,102 @@
                 EmptyDataDecls
                 NoMonomorphismRestriction
 
-    build-depends: base                          >= 4          && < 5
+    if flag(no-icu)
+        build-depends: base                          >= 4          && < 5
 
-                 , aeson
-                 , attoparsec
-                 , attoparsec-conduit
-                 , blaze-builder
-                 , blaze-html
-                 , blaze-markup
-                 , bytestring
-                 , case-insensitive
-                 , conduit
-                 , containers
-                 , data-default
-                 , directory
-                 , email-validate
-                 , filepath
-                 , hamlet
-                 -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66
-                 , hashable                      <= 1.2.0.5 || > 1.2.0.7
-                 , hjsmin
-                 , http-conduit
-                 , http-types
-                 , lifted-base
-                 , mime-mail
-                 , monad-control
-                 , network
-                 , old-locale
-                 , pandoc                        >= 1.11   && < 1.12
-                 , process
-                 , process-conduit               >= 0.5
-                 , pwstore-fast
-                 , random
-                 , shakespeare-css
-                 , shakespeare-js
-                 , shakespeare-text
-                 , tagsoup
-                 , template-haskell
-                 , text
-                 , text-icu
-                 , time
-                 , transformers
-                 , unordered-containers
-                 , vector
-                 , wai
-                 , wai-extra
-                 , warp
-                 , xss-sanitize
-                 , yaml
-                 , yesod                         >= 1.2    && < 1.3
-                 , yesod-auth
-                 , yesod-core
-                 , yesod-form
-                 , yesod-static
+                     , aeson
+                     , attoparsec
+                     , attoparsec-conduit
+                     , blaze-builder
+                     , blaze-html
+                     , blaze-markup
+                     , bytestring
+                     , case-insensitive
+                     , conduit
+                     , containers
+                     , directory
+                     , data-default
+                     , email-validate
+                     , filepath
+                     , hamlet
+                     -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66
+                     , hashable                      <= 1.2.0.5 || > 1.2.0.7
+                     , hjsmin
+                     , http-conduit
+                     , http-types
+                     , lifted-base
+                     , markdown                      >= 0.1.5
+                     , mime-mail
+                     , network
+                     , old-locale
+                     , process
+                     , process-conduit               >= 0.5
+                     , pwstore-fast
+                     , random
+                     , tagsoup
+                     , template-haskell
+                     , text
+                     , time
+                     , transformers
+                     , unordered-containers
+                     , vector
+                     , wai
+                     , wai-extra
+                     , warp
+                     , xss-sanitize                  >= 0.3.4
+                     , yaml
+                     , yesod                         >= 1.2    && < 1.3
+                     , yesod-auth
+                     , yesod-static
+    else
+        build-depends: base                          >= 4          && < 5
 
+                     , aeson
+                     , attoparsec
+                     , attoparsec-conduit
+                     , blaze-builder
+                     , blaze-html
+                     , blaze-markup
+                     , bytestring
+                     , case-insensitive
+                     , conduit
+                     , containers
+                     , data-default
+                     , directory
+                     , email-validate
+                     , filepath
+                     , hamlet
+                     -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66
+                     , hashable                      <= 1.2.0.5 || > 1.2.0.7
+                     , hjsmin
+                     , http-conduit
+                     , http-types
+                     , lifted-base
+                     , markdown                      >= 0.1.5
+                     , mime-mail
+                     , network
+                     , old-locale
+                     , process
+                     , process-conduit               >= 0.5
+                     , pwstore-fast
+                     , random
+                     , tagsoup
+                     , template-haskell
+                     , text
+                     , text-icu
+                     , time
+                     , transformers
+                     , unordered-containers
+                     , vector
+                     , wai
+                     , wai-extra
+                     , warp
+                     , xss-sanitize                  >= 0.3.4
+                     , yaml
+                     , yesod                         >= 1.2    && < 1.3
+                     , yesod-auth
+                     , yesod-static
+
 executable         notmuch-web
     if flag(library-only)
         Buildable: False
@@ -157,6 +211,9 @@
     build-depends: base
                  , notmuch-web
                  , hspec
+                 , HUnit
+                 , mime-mail
+                 , text
                  , yesod
                  , yesod-test
                  , yesod-core
diff --git a/package.sh b/package.sh
--- a/package.sh
+++ b/package.sh
@@ -1,10 +1,15 @@
 #!/bin/bash
 
+args=
+if [ "$1" != "--icu" ]; then
+	args="-fno-icu"
+fi
+
 f="notmuch-web-`grep ^version notmuch-web.cabal | awk '{print $2}'`"
 r="releases/$f"
 
 cabal clean
-cabal configure
+cabal configure $args
 cabal build
 strip dist/build/notmuch-web/notmuch-web
 mkdir -p $r
diff --git a/src/Application.hs b/src/Application.hs
--- a/src/Application.hs
+++ b/src/Application.hs
@@ -22,9 +22,9 @@
     , makeFoundation
     ) where
 
-import Import hiding (loadConfig)
-import Settings
+import Import
 import StaticFiles (staticSite)
+import System.Environment (lookupEnv)
 import Yesod.Auth (getAuth)
 import Yesod.Default.Config
 import Yesod.Default.Main
@@ -36,22 +36,16 @@
 
 -- Import all relevant handler modules here.
 -- Don't forget to add new modules to your cabal file!
+import Handler.Abook
+import Handler.Compose
 import Handler.Home
-import Handler.Tags
-import Handler.View
 import Handler.Pager
 import Handler.Raw
-import Handler.Compose
+import Handler.Tags
+import Handler.View
 
--- This line actually creates our YesodDispatch instance. It is the second half
--- of the call to mkYesodData which occurs in Foundation.hs. Please see the
--- comments there for more details.
 mkYesodDispatch "App" resourcesApp
 
--- This function allocates resources (such as a database connection pool),
--- performs initialization and creates a WAI application. This is also the
--- place to put your migrate statements to have automatic database
--- migrations handled by Yesod.
 makeApplication :: AppConfig DefaultEnv Extra -> IO Application
 makeApplication conf = do
     foundation <- makeFoundation conf
@@ -65,11 +59,22 @@
 makeFoundation conf = do
     manager <- newManager def
     s <- staticSite
+
+    -- Load message ID domain if it has not been set
     conf' <- case (extraMessageIDDomain $ appExtra conf) of
               "" -> do u <- NS.getHostName
                        return $ conf { appExtra = (appExtra conf) { extraMessageIDDomain = T.pack u} }
               _  -> return conf
-    return $ App conf' s manager $ extraHashedPwd $ appExtra conf'
+
+    -- Load google client id if it has not been set
+    conf'' <- case (extraGoogleClientId $ appExtra conf') of
+                Nothing -> do mi <- lookupEnv "NOTMUCH_WEB_GOOGLE_CLIENTID"
+                              case mi of
+                                 Just i -> return $ conf' { appExtra = (appExtra conf') { extraGoogleClientId = Just $ T.pack i } }
+                                 Nothing -> return conf'
+                Just _ -> return conf'
+
+    return $ App conf'' s manager $ extraHashedPwd $ appExtra conf''
 
 -- for yesod devel
 getApplicationDev :: IO (Int, Application)
diff --git a/src/FilterHtml.hs b/src/FilterHtml.hs
--- a/src/FilterHtml.hs
+++ b/src/FilterHtml.hs
@@ -16,72 +16,28 @@
 -}
 module FilterHtml (filterHtml) where
 
--- Based on the xss-sanitize project http://hackage.haskell.org/package/xss-sanitize
--- but a lot more restrictive.
-
 import Import
-import Data.Maybe (mapMaybe)
-import Data.String (IsString)
-import Text.HTML.SanitizeXSS (sanitizeAttribute)
+import Data.Text (Text)
+import Text.HTML.SanitizeXSS
 import Text.HTML.TagSoup
-import qualified Data.Map as Map
-import qualified Data.Text.Lazy as TL
 
 -- | Filter Html
-filterHtml :: TL.Text -> TL.Text
-filterHtml = renderTags . balance Map.empty . safeTags . parseTags
+filterHtml :: Text -> Text
+filterHtml = filterTags (balanceTags . allowedTags . safeTags)
 
 -- | List of allowed tags.
-allowedTags :: [TL.Text]
-allowedTags = ["div", "p", "br", "blockquote"]
+allowed :: [Text]
+allowed = ["div", "p", "br", "blockquote"]
 
 -- | Filter which tags are allowed
-safeTags :: [Tag TL.Text] -> [Tag TL.Text]
-safeTags [] = []
-safeTags (TagOpen name attrs:ts) | name `elem` allowedTags = TagOpen name (safeAttrs attrs) : safeTags ts
-safeTags (TagOpen _    _    :ts)                           = safeTags ts
-
-safeTags (t@(TagClose name):ts) | name `elem` allowedTags = t : safeTags ts
-safeTags   (TagClose _:ts)                                = safeTags ts
-
-safeTags (t:ts) = t : safeTags ts
-
--- | Filter the attributes.
-safeAttrs :: [Attribute TL.Text] -> [Attribute TL.Text]
-safeAttrs = sanitizeAttribute' . filterAttrs
-    where
-        filterAttrs = filter $ \(n,_) -> n `elem` okAttrs
-        okAttrs = ["style", "class"]
+allowedTags :: [Tag Text] -> [Tag Text]
+allowedTags [] = []
+allowedTags (t@(TagOpen name _):ts)
+    | name `elem` allowed = t : allowedTags ts
+    | otherwise           = allowedTags ts
 
--- | Wrapper around xss-sanitize's sanitizeAttribute.  
--- 
--- This clears bad attributes but also filters the CSS within the style attribute, removing
--- things like urls and so forth.
-sanitizeAttribute' :: [Attribute TL.Text] -> [Attribute TL.Text]
-sanitizeAttribute' = map fromStrict . mapMaybe sanitizeAttribute . map toStrict
-    where
-        fromStrict (a,b) = (TL.fromStrict a, TL.fromStrict b)
-        toStrict (a,b) = (TL.toStrict a, TL.toStrict b)
+allowedTags (t@(TagClose name):ts)
+    | name `elem` allowed = t : allowedTags ts
+    | otherwise           = allowedTags ts
 
--- Copied from xss-sanitize and updated to work with lazy text by changing the type signature.
-balance :: (IsString a, Ord a) => Map.Map a Int -> [Tag a] -> [Tag a]
-balance m [] =
-    concatMap go $ Map.toList m
-  where
-    go (name, i) | name == "br" = []
-                 | otherwise    = replicate i $ TagClose name
-balance m (t@(TagClose name):tags) =
-    case Map.lookup name m of
-        Nothing -> TagOpen name [] : TagClose name : balance m tags
-        Just i ->
-            let m' = if i == 1
-                        then Map.delete name m
-                        else Map.insert name (i - 1) m
-             in t : balance m' tags
-balance m (TagOpen name as : tags) =
-    TagOpen name as : balance m' tags
-  where
-    m' = case Map.lookup name m of
-            Nothing -> Map.insert name 1 m
-            Just i -> Map.insert name (i + 1) m
-balance m (t:ts) = t : balance m ts
+allowedTags (t:ts) = t : allowedTags ts
diff --git a/src/Foundation.hs b/src/Foundation.hs
--- a/src/Foundation.hs
+++ b/src/Foundation.hs
@@ -17,68 +17,41 @@
 module Foundation where
 
 import Prelude
+
+import Blaze.ByteString.Builder.Char.Utf8 (fromText)
+import Control.Applicative
+import Data.ByteString (ByteString)
+import Data.Maybe (isJust)
+import Data.Monoid (mappend)
+import Data.Text (Text)
+import Data.Text.Encoding (encodeUtf8)
+import Network.HTTP.Conduit (Manager)
+import Network.Wai (requestHeaders)
+import NotmuchCmd (ThreadID, MessageID)
+import Settings (widgetFile, Extra (..), development)
+import StaticFiles
+import Text.Hamlet (hamletFile)
+import Text.Jasmine (minifym)
 import Yesod
 import Yesod.Auth
-import Yesod.Static
 import Yesod.Default.Config
 import Yesod.Default.Util (addStaticContentExternal)
-import Network.HTTP.Conduit (Manager)
-import Data.Monoid (mappend)
-import qualified Settings
-import StaticFiles
-import Settings (widgetFile, Extra (..))
-import Network.Wai (requestHeaders)
-import Data.Maybe (isJust)
-import Text.Jasmine (minifym)
-import Text.Hamlet (hamletFile)
-import NotmuchCmd (ThreadID, MessageID)
-import Control.Applicative
-import Blaze.ByteString.Builder.Char.Utf8 (fromText)
-import Data.Text (Text)
+import Yesod.Static
 import qualified Crypto.PasswordStore as PS
-import qualified Data.ByteString as B
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
 
--- | The site argument for your application. This can be a good place to
--- keep settings and values requiring initialization before your application
--- starts running, such as database connections. Every handler will have
--- access to the data present here.
 data App = App
     { settings :: AppConfig DefaultEnv Extra
     , getStatic :: Static -- ^ Settings for static file serving.
     , httpManager :: Manager
-    , passwordHash :: B.ByteString -- ^ hashed password from "Crypto.PasswordStore"
+    , passwordHash :: ByteString -- ^ hashed password from "Crypto.PasswordStore"
     }
 
--- Set up i18n messages. See the message folder.
 mkMessage "App" "messages" "en"
 
--- This is where we define all of the routes in our application. For a full
--- explanation of the syntax, please see:
--- http://www.yesodweb.com/book/handler
---
--- This function does three things:
---
--- * Creates the route datatype AppRoute. Every valid URL in your
---   application can be represented as a value of this type.
--- * Creates the associated type:
---       type instance Route App = AppRoute
--- * Creates the value resourcesApp which contains information on the
---   resources declared below. This is used in Handler.hs by the call to
---   mkYesodDispatch
---
--- What this function does *not* do is create a YesodSite instance for
--- App. Creating that instance requires all of the handler functions
--- for our application to be in scope. However, the handler functions
--- usually require access to the AppRoute datatype. Therefore, we
--- split these actions into two functions and place them in separate files.
 mkYesodData "App" $(parseRoutesFile "config/routes")
 
 type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget)
 
--- Please see the documentation for the Yesod typeclass. There are a number
--- of settings which can be configured by overriding methods here.
 instance Yesod App where
     approot = ApprootMaster $ appRoot . settings
 
@@ -103,11 +76,12 @@
         master <- getYesod
         mmsg <- getMessage
         let folders = extraFolders $ appExtra $ settings master
+        sourceLink <- extraSourceLink <$> getExtra
 
         pjax <- isPjax
         if pjax
           then do pc <- widgetToPageContent widget
-                  hamletToRepHtml $ pageBody pc
+                  giveUrlRenderer $ pageBody pc
 
           else do pc <- widgetToPageContent $ do
                           addStylesheet $ StaticR css_bootstrap_min_css
@@ -121,12 +95,7 @@
 
                           $(widgetFile "default-layout")
 
-                  hamletToRepHtml $(hamletFile "templates/default-layout-wrapper.hamlet")
-
-    -- This is done to provide an optimization for serving static files from
-    -- a separate domain. Please see the staticRoot setting in Settings.hs
-    urlRenderOverride y (StaticR s) =
-        Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+                  giveUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
 
     -- The opensearch.xml file must include {searchTerms} in the url template, but
     -- the default url renderer percent encodes the braces which doesn't work.  So we
@@ -143,8 +112,8 @@
     -- and names them based on a hash of their content. This allows
     -- expiration dates to be set far in the future without worry of
     -- users receiving stale content.
-    addStaticContent = addStaticContentExternal mini base64md5 Settings.staticDir (StaticR . flip StaticRoute [])
-      where mini = if Settings.development then Right else minifym
+    addStaticContent = addStaticContentExternal mini base64md5 "static" (StaticR . flip StaticRoute [])
+      where mini = if development then Right else minifym
 
     -- Place Javascript at bottom of the body tag so the rest of the page loads first
     jsLoader _ = BottomOfBody
@@ -152,10 +121,8 @@
     -- What messages should be logged. The following includes all messages when
     -- in development, and warnings and errors in production.
     shouldLog _ _source level =
-        Settings.development || level == LevelWarn || level == LevelError
+        development || level == LevelWarn || level == LevelError
 
--- This instance is required to use forms. You can modify renderMessage to
--- achieve customized and internationalized form validation messages.
 instance RenderMessage App FormMessage where
     renderMessage _ _ = defaultFormMessage
 
@@ -168,12 +135,12 @@
 isPjax = do r <- waiRequest
             return $ isJust $ lookup "X-PJAX" $ requestHeaders r
 
-loginForm :: AForm (HandlerT App IO) B.ByteString
-loginForm = T.encodeUtf8 <$> areq passwordField pwd Nothing
+loginForm :: AForm (HandlerT App IO) ByteString
+loginForm = encodeUtf8 <$> areq passwordField pwd Nothing
   where pwd = FieldSettings (SomeMessage MsgPassword) Nothing (Just "Password") Nothing []
 
 instance YesodAuth App where
-    type AuthId App = T.Text
+    type AuthId App = Text
     loginDest _  = HomeR
     logoutDest _ = HomeR
     getAuthId (Creds _ n _) = return $ Just n
diff --git a/src/Handler/Abook.hs b/src/Handler/Abook.hs
new file mode 100644
--- /dev/null
+++ b/src/Handler/Abook.hs
@@ -0,0 +1,47 @@
+{-
+Copyright (C) 2013 John Lenz <lenz@math.uic.edu>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-}
+module Handler.Abook (getAbookQueryR) where
+
+import Import
+import Control.Exception.Lifted (try)
+import Data.Conduit.Process
+import System.Exit (ExitCode(..))
+import qualified Data.Text as T
+import qualified Data.Vector as V
+import qualified Data.Conduit.List as C
+import qualified Data.Conduit.Text as C
+
+-- | Abook returns an entry as "<email addr><tab><name><tab>".  This function parses
+-- such a line into a JSON value.
+parseLine :: T.Text -> Value
+parseLine x = case T.split (=='\t') x of
+                [] -> Null
+                [n] -> String n
+                (email:name:_) -> String $ T.concat [name, " <", email, ">"]
+
+-- | Query abook for the given string
+getAbookQueryR :: Handler Value
+getAbookQueryR = do
+    q <- fromMaybe "" <$> lookupGetParam "q"
+    let p = proc "abook" ["--mutt-query", T.unpack q]
+    rs <- try $ runResourceT $ sourceProcess p $= C.decode C.utf8 $= C.lines $$ C.consume
+    return $ case rs of
+        Left (ExitFailure 1) -> Null -- exit code 1 is for no results
+        Left e -> object [ "error" .= show e ]
+        Right [] -> Null
+        Right [_] -> Null -- first line is a blank line
+        Right (_:qs) -> Array $ V.fromList $ map parseLine qs
diff --git a/src/Handler/Compose.hs b/src/Handler/Compose.hs
--- a/src/Handler/Compose.hs
+++ b/src/Handler/Compose.hs
@@ -1,3 +1,4 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
 {-
 Copyright (C) 2013 John Lenz <lenz@math.uic.edu>
 
@@ -14,236 +15,261 @@
 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
-module Handler.Compose (getComposeR, postComposeR, getReplyR, getReplyAllR) where
+module Handler.Compose (
+    getComposeR
+  , postComposeR
+  , getReplyR
+  , getReplyAllR
+  , postPreviewMessageR
+) where
 
 import Import
-import Settings
-import Network.Mail.Mime
+import Handler.ComposeFields
+
+import Data.String (fromString)
 import Data.Time
+import Network.Mail.Mime hiding (partContent)
+import System.FilePath ((</>))
 import System.Locale
 import System.Random (randomIO)
-import System.FilePath ((</>))
-import qualified Data.ByteString as B
+import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
+import Text.Markdown
+
 import qualified Data.ByteString.Lazy as BL
+import qualified Data.CaseInsensitive as CI
 import qualified Data.Conduit.List as CL
+import qualified Data.Map as M
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
 import qualified Data.Text.Lazy as TL
 import qualified Data.Text.Lazy.Encoding as TL
 import qualified Data.Text.Lazy.IO as TL
-import qualified Text.Email.Validate as E
-import qualified Data.Map as M
-import qualified Data.CaseInsensitive as CI
-import Data.Attoparsec.Text
-import Data.String (fromString)
-import NotmuchCmd
 
--- | Parse an email address in angle brackets
-emailInBrackets :: Parser T.Text
-emailInBrackets = do
-    void $ char '<'
-    y <- takeTill (=='>') <?> "Email address"
-    void (char '>' <?> "Expecting '>'")
-    skipSpace
-    return y
+-----------------------------------------------------------------------------------------
+-- Compose Form
+-----------------------------------------------------------------------------------------
 
--- | Parses an email address, which can either just be a direct email
--- address or can be an address in the form "Name <email>"
-address :: Parser Address
-address = do
-    x <- T.strip <$> takeTill (\c -> c == '<' || c == ',')
-    y <- Just <$> emailInBrackets <|> return Nothing
-    case y of
-        Just e -> return $ Address (Just x) e
-        Nothing -> return $ Address Nothing x
+instance Eq Address where
+    (Address a1 a2) == (Address b1 b2) = a1 == b1 && a2 == b2
 
--- | Parse a list of addresses seperated by commas
-addresses :: Parser [Address]
-addresses = do as <- sepBy address (char ',')
-               endOfInput <?> "Expecting ',' or '>'"
-               return as
+-- | Lookup From addresses in the settings
+fromAddresses :: (MonadHandler m, HandlerSite m ~ App) => m (OptionList Address)
+fromAddresses = mkOptionList <$> do
+    addrs <- extraFromAddresses <$> getExtra
+    forM addrs $ \a ->
+        case parseAddress a of
+            Left err -> do
+                setMessageI err
+                return $ Option ("Invalid " <> a) (Address Nothing "") ""
+            Right a' -> return $ Option a a' a
 
--- | A version of parseOnly which includes the context of the failure.
-parseOnly' :: Parser a -> T.Text -> Either String a
-parseOnly' p t = checkRes (parse p t)
-    where checkRes result = case result of
-                              Fail _ ctx err -> Left $ show ctx ++ " " ++ err
-                              Partial f -> checkRes $ f ""
-                              Done _ x -> Right $ x
+-- | Parse an address header like To: and CC: into a list of address
+parseAddrHeader :: (MonadHandler m, HandlerSite m ~ App) => CI.CI T.Text -> Reply -> m [Address]
+parseAddrHeader hdr reply = 
+    case M.lookup hdr (replyHeaders reply) of
+        Nothing -> return []
+        Just x  -> case parseAddresses x of
+                           Left err -> do setMessageI err
+                                          return []
+                           Right addr -> return addr
 
-parseAddresses :: T.Text -> Either (SomeMessage App) (Maybe [Address])
-parseAddresses t = case parseOnly' addresses t of
-                       Left err -> Left $ fromString err
-                       Right [] -> Right $ Nothing
-                       Right addrs -> Just <$> mapM checkAddr addrs
-    where
-        checkAddr a@(Address _ e) | E.isValid (T.encodeUtf8 e) = Right a
-        checkAddr (Address _ e)   | otherwise = Left $ SomeMessage $ MsgInvalidEmail e
+-- | Search for the first part which is a body
+findBodyText :: [MessagePart] -> [T.Text]
+findBodyText [] = []
+findBodyText ((MessagePart {partContent = ContentText t}):_) = map (\x -> "> " <> x <> "\n") $ T.lines t
+findBodyText ((MessagePart {partContent = ContentMsgRFC822 _}):_) = []
+findBodyText ((MessagePart {partContent = ContentMultipart sub}):ms) =
+    case findBodyText sub of
+        [] -> findBodyText ms
+        x  -> x
 
-showAddresses :: [Address] -> T.Text
-showAddresses as = T.intercalate ", " $ map showA as
-    where showA (Address {addressName = Just name, addressEmail = e}) = T.concat [name, " <", e, ">"]
-          showA (Address {addressName = Nothing, addressEmail = e}) = e
+-- | Parse a reply into a mail message and the reply body
+parseReply :: (MonadHandler m, HandlerSite m ~ App) => Reply -> m (Mail, T.Text)
+parseReply reply = do
+    to <- parseAddrHeader "to" reply
+    cc <- parseAddrHeader "cc" reply
 
-addressField :: Field (HandlerT App IO) [Address]
-addressField = Field { fieldParse = \x _ -> case x of
-                                                [] -> return $ Right Nothing
-                                                ("":_) -> return $ Right Nothing
-                                                (n:_) -> return $ parseAddresses n
-                     , fieldView = \theId name attrs val isReq -> [whamlet|
-<input id=#{theId} name=#{name} *{attrs} type=text :isReq:required value=#{either id showAddresses val}>
-|]
-                     , fieldEnctype = UrlEncoded
-                     }
+    let extra = foldr M.delete (replyHeaders reply) ["to", "cc", "subject", "from"]
 
-header :: Parser (B.ByteString, T.Text)
-header = do
-    k <- takeWhile1 (\c -> not (isEndOfLine c) && c /= ':')
-    void $ char ':'
-    skipSpace
-    v <- takeTill isEndOfLine
-    return (T.encodeUtf8 k, v)
+    let mail = Mail (Address Nothing "")
+                    to
+                    cc
+                    [] -- bcc
+                    [(T.encodeUtf8 $ CI.original k, v) | (k,v) <- M.toList extra]
+                    [] -- parts
 
-headers :: Parser [(B.ByteString, T.Text)]
-headers = sepBy header endOfLine <?> "Headers"
+    tz <- liftIO getCurrentTimeZone
+    let t = utcToZonedTime tz $ messageTime $ replyOriginal reply
+    let ts = formatTime defaultTimeLocale "%a %b %e %R %z %Y" t
 
-parseHeaders :: T.Text -> Either (SomeMessage App) (Maybe [(B.ByteString,T.Text)])
-parseHeaders t = case parseOnly' headers t of
-                     Left err -> Left $ fromString err
-                     Right [] -> Right $ Nothing
-                     Right h -> Right $ Just h
+    let body = T.concat $
+                [ "On "
+                , T.pack ts
+                , ", "
+                , fromMaybe "" $ M.lookup "from" $ messageHeaders $ replyOriginal reply
+                , " wrote:\n"
+                ] ++ (findBodyText $ messageBody $ replyOriginal reply)
 
-showHeaders :: [(B.ByteString, T.Text)] -> T.Text
-showHeaders hs = T.intercalate "\n" $ map showH hs
-    where showH (x,y) = T.concat [T.decodeUtf8 x, ": ", y]
+    return (mail, body)
 
-headerField :: Field (HandlerT App IO) [(B.ByteString,T.Text)]
-headerField = Field { fieldParse = \x _ -> case x of
-                                                [] -> return $ Right Nothing
-                                                ("":_) -> return $ Right Nothing
-                                                (n:_) -> return $ parseHeaders n
-                     , fieldView = \theId name attrs val isReq -> [whamlet|
-<textarea id=#{theId} name=#{name} *{attrs} rows=4 cols=50 wrap=off :isReq:required>
-    #{either id showHeaders val}
-|]
-                     , fieldEnctype = UrlEncoded
-                     }
+data EmailBodyFormat = EmailBodyQuotedPrintable
+                     | EmailBodyPlain
+                     | EmailBodyMarkdown
+  deriving (Eq, Enum, Bounded)
 
-multiFile :: Field (HandlerT master IO) [FileInfo]
-multiFile = Field p view Multipart
-    where
-        p _ fs = return $ Right $ Just fs
-        view fId name attrs _ _ = [whamlet|
-<input type=file name=#{name} ##{fId} multiple *{attrs}>
-|]
+instance Show EmailBodyFormat where
+    show EmailBodyQuotedPrintable = "Send as text/plain, UTF-8 encoded with quoted printable"
+    show EmailBodyPlain = "Send as text/plain, UTF-8, no encoding"
+    show EmailBodyMarkdown = "Parse body as markdown; send text and html parts"
 
-findBodyText :: [MessagePart] -> Maybe T.Text
-findBodyText [] = Nothing
-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentText t}):_) = Just t
-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentMsgRFC822 _}):_) = Nothing
-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentMultipart sub}):ms) =
-    case findBodyText sub of
-        Nothing -> findBodyText ms
-        Just x -> Just x
+markdownSettings :: MarkdownSettings
+markdownSettings = def { msXssProtect = False -- Input is trusted from compose form
+                       , msBlankBeforeBlockquote = False
+                       }
 
-replyBody :: Reply -> T.Text
-replyBody (Reply {replyOriginal = m})  = T.concat [onmsg, "\n", bdy]
-  where
-    origfrom = maybe "" id $ M.lookup "from" $ messageHeaders m
-    onmsg = T.concat ["On ", T.pack (show $ messageTime m), ", ", origfrom, " wrote: "]
-    origlines = T.lines $ maybe "" id $ findBodyText $ messageBody m
-    addquote = map (T.append "> ") origlines
-    bdy = T.unlines addquote
+-- | Read the body as markdown and create html
+markdownToHtml :: T.Text -> Html
+markdownToHtml = markdown markdownSettings . TL.fromStrict
 
-fileToAttach :: FileInfo -> ResourceT IO Alternatives
-fileToAttach f = do
-    content <- fileSource f $$ CL.consume
-    return [Part (fileContentType f) Base64 (Just $ fileName f) [] (BL.fromChunks content)]
+-- | Create the body of the outgoing message
+createBody :: (MonadHandler m, HandlerSite m ~ App)
+           => EmailBodyFormat -> Textarea -> [FileInfo] -> m [Alternatives]
+createBody fmt bodytext attach = do
+    attachParts <- liftResourceT $ forM attach $ \f -> do
+        content <- fileSource f $$ CL.consume
+        return [Part (fileContentType f) Base64 (Just $ fileName f) [] (BL.fromChunks content)]
 
-composeForm :: Address -> Maybe Reply -> Form Mail
-composeForm from mreply fmsg = do
-    let mheaders = replyHeaders <$> mreply
-        mto = M.lookup "to" =<< mheaders
-        mtoaddr = either (const Nothing) id . parseAddresses =<< mto
-        mcc = M.lookup "cc" =<< mheaders
-        mccaddr = either (const Nothing) Just . parseAddresses =<< mcc
-        --from = maybe (Just f) $ M.lookup "from" =<< extraheaders
-        msubj = M.lookup "subject" =<< mheaders
-        mbody = Textarea . replyBody <$> mreply
-        mextramap = M.delete "to" . M.delete "cc" . M.delete "subject" . M.delete "from" <$> mheaders
-        mextra = (\hs -> Just [ (T.encodeUtf8 $ CI.original k, v) | (k,v) <- M.toList hs ]) <$> mextramap
+    let b = Part "text/plain; charset=UTF-8" None Nothing [] $ BL.fromChunks [T.encodeUtf8 $ unTextarea bodytext]
+        bq = b { partEncoding = QuotedPrintableText }
+        html = renderHtml $ markdownToHtml $ unTextarea bodytext
+        hpart = Part "text/html; charset=UTF-8" QuotedPrintableText Nothing [] html
 
-    (to,toView) <- mreq addressField (FieldSettings "To" Nothing (Just "to") Nothing []) mtoaddr
-    (cc,ccView) <- mopt addressField (FieldSettings "CC" Nothing (Just "cc") Nothing []) mccaddr
-    (bcc,bccView) <- mopt addressField (FieldSettings "BCC" Nothing (Just "bcc") Nothing []) Nothing
-    (subject,sView) <- mreq textField (FieldSettings (SomeMessage MsgSubject) Nothing (Just "subject") Nothing []) msubj
-    (head,hView) <- mopt headerField (FieldSettings (SomeMessage MsgExtraHeader) Nothing (Just "extraheaders") Nothing []) mextra
-    (body,bView) <- mreq textareaField (FieldSettings "Body" Nothing (Just "body") Nothing []) mbody
-    (attach,attachView) <- mopt multiFile (FieldSettings (SomeMessage MsgAttach) Nothing (Just "attach") Nothing []) Nothing
+    let body = case fmt of
+                EmailBodyQuotedPrintable -> [bq]
+                EmailBodyPlain -> [b]
+                EmailBodyMarkdown -> [bq, hpart]
 
-    attachParts <- case attach of
-                       FormSuccess (Just xs) -> liftResourceT $ mapM fileToAttach xs
-                       _ -> return []
+    return $ body : attachParts
 
-    let mkParts b = [[Part "text/plain" QuotedPrintableText Nothing [] $ BL.fromChunks [T.encodeUtf8 $ unTextarea b]]] ++ attachParts
-        mkHeaders s e = ("Subject", s) : maybe [] id e
-        mail = Mail <$> pure from
+-- | Create a new message ID
+messageID :: (MonadHandler m, HandlerSite m ~ App) => m T.Text
+messageID = do t <- liftIO getCurrentTime
+               let ts = formatTime defaultTimeLocale "%s" t
+               i <- abs <$> liftIO (randomIO :: IO Int)
+               domain <- extraMessageIDDomain <$> getExtra
+               case domain of
+                 "" -> return ""
+                 _  -> return $ T.concat ["<notmuch-web-", T.pack ts, ".", T.pack (show i), "@", domain, ">"]
+
+-- | Create a field settings from a string.  The default IsString instance does not set the id.
+fStr :: T.Text -> FieldSettings site
+fStr i = FieldSettings (fromString $ T.unpack i) Nothing (Just i) Nothing []
+
+-- | Create a field setting from a message
+fI :: AppMessage -> T.Text -> FieldSettings App
+fI m i = FieldSettings (SomeMessage m) Nothing (Just i) Nothing []
+
+-- | A helper widget to display a form element in bootstrap markup
+formElem :: Bool -> FieldView App -> Widget
+formElem includeHelp v = [whamlet|
+<div .control-group>
+    <label .control-label for=#{fvId v}>#{fvLabel v}
+    <div .controls>
+        ^{fvInput v}
+        $if includeHelp
+            <i .addr-help .icon-question-sign data-title=_{MsgAddrHelpTitle} data-content=_{MsgAddrHelp}>
+|]
+
+-- | A widget to select the address book type
+addressBookWidget :: Widget
+addressBookWidget = do
+    $(widgetFile "abook")
+    mID <- extraGoogleClientId <$> getExtra
+    case mID of
+        Nothing -> $(widgetFile "plain-contacts")
+        Just clientID -> $(widgetFile "google-contacts")
+
+-- | The compose form
+composeForm :: Maybe Reply -> Form Mail
+composeForm mreply fmsg = do
+    mmail <- lift $ maybe (return Nothing) (\x -> Just <$> parseReply x) mreply
+
+    (from,fromView) <- mreq (selectField fromAddresses) (fStr "From") Nothing
+    (to,toView) <- mreq addressField (fStr "To") (mailTo . fst <$> mmail)
+    (cc,ccView) <- mopt addressField (fStr "CC") (Just . mailCc . fst <$> mmail)
+    (bcc,bccView) <- mopt addressField (fStr "BCC") Nothing
+    (subject,sView) <- mreq textField (fI MsgSubject "subj") $ M.lookup "subject" =<< (replyHeaders <$> mreply)
+    (head,hView) <- mopt headerField (fI MsgExtraHeader "hdrs") (Just . mailHeaders . fst <$> mmail)
+    (bfmt,fmtView) <- mreq (selectField optionsEnum) (fI MsgBodyFormat "bdyfmt") Nothing
+    (body,bView) <- mreq textareaField (fI MsgBody "Body") (Textarea . snd <$> mmail)
+    (attach,attachView) <- mopt multiFile (fI MsgAttach "attch") Nothing
+
+    parts <- case (,,) <$> bfmt <*> body <*> attach of
+               FormSuccess (f,b,a) -> FormSuccess <$> createBody f b (fromMaybe [] a)
+               FormFailure err     -> return $ FormFailure err
+               FormMissing         -> return $ FormMissing
+               
+    mid <- lift messageID
+
+    let mkHeaders s e = ("Subject", s) : ("Message-ID", mid) : fromMaybe [] e
+        mail = Mail <$> from
                     <*> to
-                    <*> (maybe [] id <$> cc)
-                    <*> (maybe [] id <$> bcc)
+                    <*> (fromMaybe [] <$> cc)
+                    <*> (fromMaybe [] <$> bcc)
                     <*> (mkHeaders <$> subject <*> head)
-                    <*> (mkParts <$> body)
+                    <*> parts
 
-        widget = [whamlet|
- #{fmsg}
- $forall v <- [toView, ccView, bccView, sView, hView]
-    <div .control-group .span10>
-        <label .control-label for=#{fvId v}>#{fvLabel v}
-        <div .controls>
-            ^{fvInput v}
- <div .control-group>
-    ^{fvInput bView}
- <div .control-group>
-    <label .control-label for=#{fvId attachView}>#{fvLabel attachView}
-    <div .controls>
-        ^{fvInput attachView}
+        widget = [whamlet|#{fmsg}
+<div .row>
+    <div .span6>
+        ^{formElem False fromView}
+        ^{formElem True toView}
+        ^{formElem False ccView}
+        ^{formElem False bccView}
+    <div .span6>
+        ^{addressBookWidget}
+        $forall v <- [hView, fmtView]
+            ^{formElem False v}
+^{formElem False sView}
+^{formElem False bView}
+^{formElem False attachView}
 |]
     return (mail, widget)
 
--- | Lookup the configured from address
-fromAddress :: Handler Address
-fromAddress = do
-    from <- extraFromAddress <$> getExtra
-    case parseAddresses from of
-        Left err -> invalidArgsI [err]
-        Right (Just [x]) -> return x
-        Right _ -> invalidArgs ["From address is invalid"]
+previewForm :: Form T.Text
+previewForm = renderDivs $ areq textField (fStr "previewtext") Nothing
 
-getComposeR :: Handler RepHtml
+-----------------------------------------------------------------------------------------
+-- Handlers
+-----------------------------------------------------------------------------------------
+
+getComposeR :: Handler Html
 getComposeR = do
-    from <- fromAddress
-    ((_,widget),enctype) <- runFormPost $ composeForm from Nothing
+    ((_,widget),enctype) <- runFormPost $ composeForm Nothing
+    (previewWidget,previewEnctype) <- generateFormPost previewForm
     defaultLayout $ do
         setTitleI MsgCompose
         let err = [] :: [String]
         $(widgetFile "compose")
 
-replyHandler :: ReplyTo -> MessageID -> Handler RepHtml
+-- | Helper function for reply and reply all
+replyHandler :: ReplyTo -> MessageID -> Handler Html
 replyHandler rto m = do
-    from <- fromAddress
     reply <- notmuchReply m rto
-    ((_,widget),enctype) <- runFormPost $ composeForm from $ Just reply
+    ((_,widget),enctype) <- runFormPost $ composeForm $ Just reply
+    (previewWidget,previewEnctype) <- generateFormPost previewForm
     defaultLayout $ do
         setTitleI MsgCompose
         let err = [] :: [String]
         $(widgetFile "compose")
 
-getReplyR :: MessageID -> Handler RepHtml
+getReplyR :: MessageID -> Handler Html
 getReplyR = replyHandler ReplySender
 
-getReplyAllR :: MessageID -> Handler RepHtml
+getReplyAllR :: MessageID -> Handler Html
 getReplyAllR = replyHandler ReplyAll
 
+-- | Create a unique filename and a date header from the current time
 filenameAndDate :: IO (FilePath, TL.Text)
 filenameAndDate = do t <- getCurrentTime >>= utcToLocalZonedTime
                      let ts = formatTime defaultTimeLocale "%F-%T%z" t
@@ -251,23 +277,13 @@
                      i <- randomIO :: IO Int
                      return (ts ++ "-" ++ show i, "Date: " <> TL.pack ds <> "\n")
 
-messageID :: Handler T.Text
-messageID = do t <- liftIO getCurrentTime
-               let ts = formatTime defaultTimeLocale "%s" t
-               i <- abs <$> liftIO (randomIO :: IO Int)
-               domain <- extraMessageIDDomain <$> getExtra
-               case domain of
-                 "" -> return ""
-                 _  -> return $ T.concat ["<notmuch-web-", T.pack ts, ".", T.pack (show i), "@", domain, ">"]
-
-postComposeR :: Handler RepHtml
+postComposeR :: Handler Html
 postComposeR = do
-    from <- fromAddress
-    ((result,widget),enctype) <- runFormPost $ composeForm from Nothing
+    ((result,widget),enctype) <- runFormPost $ composeForm Nothing
+    (previewWidget,previewEnctype) <- generateFormPost previewForm
     case result of
         FormSuccess m -> do
-            mid <- messageID
-            msg <- liftIO $ renderMail' $ m {mailHeaders = mailHeaders m ++ [("Message-ID", mid)]}
+            msg <- liftIO $ renderMail' m
             let tmsg = TL.decodeUtf8 msg
 
             when production $ do
@@ -286,3 +302,11 @@
         FormFailure err -> defaultLayout $ do
             setTitleI MsgCompose
             $(widgetFile "compose")
+
+postPreviewMessageR :: Handler Html
+postPreviewMessageR = do
+    ((result,_),_) <- runFormPost previewForm
+    case result of
+        FormMissing -> invalidArgs ["Form is missing"]
+        FormFailure err -> invalidArgs err
+        FormSuccess h -> return $ markdownToHtml h
diff --git a/src/Handler/ComposeFields.hs b/src/Handler/ComposeFields.hs
new file mode 100644
--- /dev/null
+++ b/src/Handler/ComposeFields.hs
@@ -0,0 +1,168 @@
+{-
+Copyright (C) 2013 John Lenz <lenz@math.uic.edu>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-}
+module Handler.ComposeFields (
+    addressField
+  , parseAddress
+  , parseAddresses
+  , showAddress
+  , headerField
+  , multiFile
+) where
+
+import Import
+import StaticFiles
+
+import Data.Attoparsec.Text
+import Data.String (fromString)
+import Network.Mail.Mime (Address(..))
+
+import qualified Data.ByteString as B
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import qualified Text.Email.Validate as E
+
+-- | Parse an email address in angle brackets
+emailInBrackets :: Parser T.Text
+emailInBrackets = do
+    void $ char '<'
+    y <- takeTill (=='>') <?> "Email address"
+    void (char '>' <?> "Expecting '>'")
+    skipSpace
+    return y
+
+quotedName :: Parser T.Text
+quotedName = do
+    void $ char '"'
+    x <- takeTill (=='"')
+    void $ char '"'
+    skipSpace
+    let parts = T.split (==',') x
+    return $ case parts of
+                (p:ps) -> T.unwords $ map T.strip $ ps ++ [p]
+                [] -> ""
+
+unquotedName :: Parser T.Text
+unquotedName = T.strip <$> takeTill (\x -> x == '<' || x == ',')
+
+-- | Parses an email address, which can either just be a direct email
+-- address or can be an address in the form "Name <email>"
+address :: Parser Address
+address = do
+    skipSpace
+    x <- quotedName <|> unquotedName
+    y <- Just <$> emailInBrackets <|> return Nothing
+    case y of
+        Just e -> return $ Address (Just x) e
+        Nothing -> return $ Address Nothing x
+
+-- | Parse a list of addresses seperated by commas
+addresses :: Parser [Address]
+addresses = do as <- address `sepBy1` char ','
+               endOfInput <?> "Expecting ',' or '>'"
+               return as
+
+-- | Checks if an email is valid
+checkAddr :: Address -> Either (SomeMessage App) Address
+checkAddr a@(Address _ e) | E.isValid (T.encodeUtf8 e) = Right a
+checkAddr (Address _ e) = Left $ SomeMessage $ MsgInvalidEmail e
+
+-- | Parse a single address
+parseAddress :: T.Text -> Either (SomeMessage App) Address
+parseAddress t = case parseOnly' address t of
+                       Left err -> Left $ fromString $ concat ["Error parsing ", T.unpack t, ": ", err]
+                       Right a  -> checkAddr a
+
+-- | Parse a list of addresses separated by commas
+parseAddresses :: T.Text -> Either (SomeMessage App) [Address]
+parseAddresses t = case parseOnly' addresses t of
+                       Left err -> Left $ fromString $ concat ["Error parsing ", T.unpack t, ": ", err]
+                       Right [(Address Nothing "")] -> Right []
+                       Right a -> mapM checkAddr a
+
+showAddress :: Address -> T.Text
+showAddress (Address {addressName = Just name, addressEmail = e}) = T.concat [name, " <", e, ">"]
+showAddress (Address {addressName = Nothing, addressEmail = e}) = e
+
+addrWidget :: FieldViewFunc (HandlerT App IO) [Address]
+addrWidget theID name attrs val isReq = do 
+    addStylesheet $ StaticR css_select2_css
+    addScript $ StaticR js_select2_3_4_1_min_js
+
+    let addrs = either id (T.intercalate "," . map showAddress) val
+
+    [whamlet|
+      <input type=text ##{theID} name=#{name} .address-field :isReq:required value="#{addrs}" *{attrs}>
+    |]
+
+addressField :: Field (HandlerT App IO) [Address]
+addressField = Field
+  { fieldParse = \addr _ -> case addr of
+                              []    -> return $ Right Nothing
+                              (a:_) -> return $ Just <$> parseAddresses a
+  , fieldView = addrWidget
+  , fieldEnctype = UrlEncoded
+  }
+
+-- | Parse a header
+header :: Parser (B.ByteString, T.Text)
+header = do
+    k <- takeWhile1 (\c -> not (isEndOfLine c) && c /= ':')
+    void $ char ':'
+    skipSpace
+    v <- takeTill isEndOfLine
+    return (T.encodeUtf8 k, v)
+
+-- | Parse a list of headers
+headers :: Parser [(B.ByteString, T.Text)]
+headers = sepBy header endOfLine <?> "Headers"
+
+headerField :: Field (HandlerT App IO) [(B.ByteString,T.Text)]
+headerField = Field 
+    { fieldParse = \x _ -> case x of
+                             [] -> return $ Right Nothing
+                             ("":_) -> return $ Right Nothing
+                             (n:_) -> return $ case parseOnly' headers n of
+                                                 Left err -> Left $ fromString err
+                                                 Right [] -> Right $ Nothing
+                                                 Right h  -> Right $ Just h
+    , fieldView = \theId name attrs val isReq -> do
+        let hdrs = case val of
+                     Left txt -> txt
+                     Right vals -> T.intercalate "\n" [ T.decodeUtf8 x <> ": " <> y | (x,y) <- vals]
+        [whamlet|
+<textarea id=#{theId} name=#{name} *{attrs} rows=4 cols=50 wrap=off :isReq:required>
+    #{hdrs}
+|]
+    , fieldEnctype = UrlEncoded
+    }
+
+multiFile :: Field (HandlerT master IO) [FileInfo]
+multiFile = Field p view Multipart
+    where
+        p _ fs = return $ Right $ Just fs
+        view fId name attrs _ _ = [whamlet|
+<input type=file name=#{name} ##{fId} multiple *{attrs}>
+|]
+
+
+-- | A version of parseOnly which includes the context of the failure.
+parseOnly' :: Parser a -> T.Text -> Either String a
+parseOnly' p t = checkRes (parse p t)
+    where checkRes result = case result of
+                              Fail _ ctx err -> Left $ show ctx ++ " " ++ err
+                              Partial f -> checkRes $ f ""
+                              Done _ x -> Right x
diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs
--- a/src/Handler/Home.hs
+++ b/src/Handler/Home.hs
@@ -23,13 +23,11 @@
   ) where
 
 import Import
-import Settings
+import Handler.Tags
+
 import Text.Hamlet (hamletFile)
 import qualified Data.Text as T
 
-import Handler.Tags
-import NotmuchCmd
-
 searchTable :: String -> Widget
 searchTable s = do
     tagHeader
@@ -37,7 +35,7 @@
     $(widgetFile "search")
 
 -- | The GET handler for the home page
-getHomeR :: Handler RepHtml
+getHomeR :: Handler Html
 getHomeR = defaultLayout $ do
     folders <- extraFolders <$> getExtra
     case folders of
@@ -46,7 +44,7 @@
         _ -> setTitle "Notmuch"
 
 -- | The GET handler for the search page
-getSearchR :: String -> Handler RepHtml
+getSearchR :: String -> Handler Html
 getSearchR s = defaultLayout $ do
     setTitle $ toHtml $ "Notmuch - " ++ s
     searchTable s
diff --git a/src/Handler/Pager.hs b/src/Handler/Pager.hs
--- a/src/Handler/Pager.hs
+++ b/src/Handler/Pager.hs
@@ -18,16 +18,13 @@
 module Handler.Pager (getThreadPagerR) where
 
 import Import
-import Prelude (tail)
-import Settings
 import StaticFiles
-import NotmuchCmd
-import Data.Maybe (listToMaybe)
 import Handler.View
+
+import Data.Aeson (encode)
+import Text.Blaze (unsafeLazyByteString)
 import qualified Data.Text as T
 import qualified Data.HashMap.Strict as H
-import Text.Blaze (unsafeLazyByteString)
-import Data.Aeson (encode)
 
 encodeSearch :: (Route App -> T.Text) -> [SearchResult] -> Html
 encodeSearch ur search = unsafeLazyByteString $ encode $ map enc search
@@ -37,10 +34,10 @@
                   Object o -> o
                   _ -> error "Search is not an object"
 
-getThreadPagerR :: String -> Handler RepHtml
+getThreadPagerR :: String -> Handler Html
 getThreadPagerR s = defaultLayout $ do
     search <- notmuchSearch s
     ur <- getUrlRender
     retags <- extraRetag <$> getExtra
-    addScript $ StaticR js_jquery_pjax_1_7_0_js
+    addScript $ StaticR js_jquery_pjax_1_7_3_js
     $(widgetFile "pager")
diff --git a/src/Handler/Raw.hs b/src/Handler/Raw.hs
--- a/src/Handler/Raw.hs
+++ b/src/Handler/Raw.hs
@@ -17,14 +17,13 @@
 module Handler.Raw (getRawCommandR, postRawCommandR) where
 
 import Import
-import NotmuchCmd
 import qualified Data.Text as T
 
 rawForm :: AForm (HandlerT App IO) String
 rawForm = T.unpack <$> areq textField cmd Nothing
     where cmd = FieldSettings (SomeMessage MsgRaw) Nothing (Just "Command") Nothing []
 
-getRawCommandR :: Handler RepHtml
+getRawCommandR :: Handler Html
 getRawCommandR = defaultLayout $ do
     setTitleI MsgRaw
     ((result,widget),enctype) <- liftHandlerT $ runFormPost $ renderBootstrap rawForm
@@ -41,7 +40,7 @@
     <input .btn .primary type=submit value=_{MsgRunCmd}>
 |]
 
-postRawCommandR :: Handler RepHtml
+postRawCommandR :: Handler Html
 postRawCommandR = do
     ((result,_),_) <- runFormPost $ renderBootstrap rawForm
     command <- case result of
diff --git a/src/Handler/Tags.hs b/src/Handler/Tags.hs
--- a/src/Handler/Tags.hs
+++ b/src/Handler/Tags.hs
@@ -24,14 +24,20 @@
 ) where
 
 import Import
-import Settings
+--import Data.Hashable (hash)
 import qualified Data.Text as T
 
-import NotmuchCmd
-
 displayTag :: T.Text -> WidgetT App IO ()
 displayTag "unread" = [whamlet|<span .label .label-important .label-tag>unread|]
-displayTag t = [whamlet|<span .label .label-info .label-tag>#{t}|]
+displayTag t        = [whamlet|<span .label .label-info .label-tag>#{t}|]
+{-
+displayTag t = case hash t `mod` 5 of
+                0 -> [whamlet|<span .label .label-success .label-tag>#{t}|]
+                1 -> [whamlet|<span .label .label-tag>#{t}|]
+                2 -> [whamlet|<span .label .label-warning .label-tag>#{t}|]
+                3 -> [whamlet|<span .label .label-inverse .label-tag>#{t}|]
+                _ -> [whamlet|<span .label .label-info .label-tag>#{t}|]
+-}
 
 -- | A widget consisting of the retag buttons followed by the current tag list.
 tagWidget :: Either SearchResult Message -> WidgetT App IO ()
@@ -48,7 +54,7 @@
   retags <- extraRetag <$> getExtra
   [whamlet|
 <span .tags>
-    <div .btn-group>
+    <span .btn-group .hide-noscript>
         $forall r <- retags
             <button .btn .retag-button .btn-link title="#{retagName r}" data-notmuch-url="@{url (retagName r)}">
                 <i class="#{retagIcon r}">
@@ -79,7 +85,7 @@
     (customWidget,customEnc) <- liftHandlerT $ generateFormPost customRetagForm
     $(widgetFile "tag-header")
 
-postRetagThreadR :: ThreadID -> T.Text -> Handler RepJson
+postRetagThreadR :: ThreadID -> T.Text -> Handler Value
 postRetagThreadR t name = do
     ((result,_),_) <- runFormPost retagForm
     case result of
@@ -92,9 +98,9 @@
     case retag of
         [] -> notFound
         (r:_) -> do notmuchTagThread (retagAdd r) (retagRemove r) t
-                    return $ repJson $ toJSON r
+                    return $ toJSON r
 
-postRetagMessageR :: MessageID -> T.Text -> Handler RepJson
+postRetagMessageR :: MessageID -> T.Text -> Handler Value
 postRetagMessageR m name = do
     ((result,_),_) <- runFormPost retagForm
     case result of
@@ -107,9 +113,9 @@
     case retag of
         [] -> notFound
         (r:_) -> do notmuchTagMessage (retagAdd r) (retagRemove r) m
-                    return $ repJson $ toJSON r
+                    return $ toJSON r
 
-postCustomRetagThreadR :: ThreadID -> Handler RepJson
+postCustomRetagThreadR :: ThreadID -> Handler Value
 postCustomRetagThreadR t = do
     ((result,_),_) <- runFormPost customRetagForm
     (add, remove) <- case result of
@@ -120,9 +126,9 @@
         rem' = maybe [] words remove
 
     notmuchTagThread add' rem' t
-    return $ repJson $ object [ "add" .= add', "remove" .= rem' ]
+    return $ object [ "add" .= add', "remove" .= rem' ]
 
-postCustomRetagMessageR :: MessageID -> Handler RepJson
+postCustomRetagMessageR :: MessageID -> Handler Value
 postCustomRetagMessageR m = do
     ((result,_),_) <- runFormPost customRetagForm
     (add, remove) <- case result of
@@ -133,4 +139,4 @@
         rem' = maybe [] words remove
 
     notmuchTagMessage add' rem' m
-    return $ repJson $ object [ "add" .= add', "remove" .= rem' ]
+    return $ object [ "add" .= add', "remove" .= rem' ]
diff --git a/src/Handler/View.hs b/src/Handler/View.hs
--- a/src/Handler/View.hs
+++ b/src/Handler/View.hs
@@ -23,52 +23,56 @@
 ) where
 
 import Import
-import Blaze.ByteString.Builder (fromByteString)
-import Control.Monad (replicateM, unless)
-import Data.List (find)
 import FilterHtml
+import StaticFiles
 import Handler.Tags
+
+import Blaze.ByteString.Builder (fromByteString)
 import Network.HTTP.Types (status200)
 import Network.Wai (Response(..))
-import NotmuchCmd
-import Settings
-import StaticFiles
-import Text.Blaze.Html5 (preEscapedToHtml)
-import Text.Pandoc
+import Text.Blaze.Html.Renderer.Text (renderHtml)
+import Text.Markdown
 import qualified Data.ByteString as B
-import qualified Data.Conduit as C
+import qualified Data.CaseInsensitive as CI
 import qualified Data.Conduit.List as C
 import qualified Data.Conduit.Text as C
+import qualified Data.Map as M
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
 import qualified Data.Text.Lazy as TL
-import qualified Data.Text.ICU.Convert as ICU
-import qualified Data.Map as M
-import qualified Data.Set as S
 import qualified Data.Tree as TR
-import qualified Data.CaseInsensitive as CI
 
+#ifdef USE_ICU
+import qualified Data.Text.ICU.Convert as ICU
+#endif
+
 decodePart :: (MonadLogger m, MonadHandler m) 
-           => Maybe T.Text -> C.Source (C.ResourceT IO) B.ByteString -> m TL.Text
+           => Maybe (CI.CI T.Text) -> Source (ResourceT IO) B.ByteString -> m TL.Text
 decodePart charset src = case charset of
                             Just "ISO-8859-1" -> decodeConduit C.iso8859_1
                             Just "UTF-8"      -> decodeConduit C.utf8
-                            Just x            -> decodeICU x
-                            Nothing           -> decodeConduit C.utf8
+#ifdef USE_ICU
+                            Just x            -> decodeICU $ CI.original x
+#endif
+                            _                 -> decodeConduit C.utf8
+
   where decodeConduit c = TL.fromChunks <$> liftResourceT (src $= C.decode c $$ C.consume)
+
+#ifdef USE_ICU
         decodeICU x = do $(logInfo) ("Decoding using ICU: " `T.append` x)
                          raw <- liftResourceT (src $$ C.consume)
                          c <- liftIO $ ICU.open (T.unpack x) (Just True)
-                         return $ TL.fromChunks [ICU.toUnicode c $ B.concat raw]
+                         return $ TL.fromStrict $ ICU.toUnicode c $ B.concat raw
+#endif
 
 messagePart :: MessageID -> Bool -> MessagePart -> Widget
 
 messagePart mid _ p@(MessagePart {partContentType = "text/html"}) = do
     let ((_ :: IO MessagePart), src) = notmuchMessagePart mid $ partID p
-    html <- decodePart (partContentCharset p) src
+    html <- TL.toStrict <$> decodePart (partContentCharset p) src
     [whamlet|
 <div .message-part .message-html>
-    #{preEscapedToHtml $ filterHtml html}
+    #{preEscapedToMarkup $ filterHtml html}
 |]
 
 messagePart mid _ m@(MessagePart {partContent = ContentText ""}) = [whamlet|
@@ -80,7 +84,7 @@
               #{f}
           $of Nothing
               <span>No filename
-    (#{partContentType m})
+    (#{CI.original $ partContentType m})
 |]
 
 -- Text which is part of an alternative
@@ -92,7 +96,7 @@
 
 -- Text not part of an alternative
 messagePart _ False (MessagePart {partContent = ContentText txt}) = do
-    let html = TL.pack $ writeHtmlString pandocWriterOpts $ readMarkdown pandocReaderOpts $ T.unpack txt
+    let html = TL.toStrict $ renderHtml $ markdown markdownSettings $ TL.fromStrict txt
     htmlId <- newIdent
     txtId <- newIdent
     [whamlet|
@@ -104,7 +108,7 @@
             <a data-toggle=tab data-target="##{txtId}">Text
     <div .tab-content>
         <div .tab-pane ##{htmlId}>
-            #{preEscapedToHtml $ filterHtml html}
+            #{preEscapedToMarkup $ filterHtml html}
         <div .tab-pane .active ##{txtId}>
             <pre>
                 #{txt}
@@ -123,7 +127,7 @@
     <ul .nav .nav-tabs>
         $forall (i,p) <- zip ids alternatives
             <li :isActive p:.active>
-                <a data-toggle=tab data-target="##{i}">#{partContentType p}
+                <a data-toggle=tab data-target="##{i}">#{CI.original $ partContentType p}
     <div .tab-content>
         $forall (i,p) <- zip ids alternatives
             <div .tab-pane :isActive p:.active ##{i}>
@@ -194,7 +198,7 @@
     tagHeader
     $(widgetFile "thread-header")
 
-getThreadR :: ThreadID -> Handler RepHtml
+getThreadR :: ThreadID -> Handler Html
 getThreadR t = defaultLayout $ do
     pjax <- isPjax
     unless pjax threadHeader
@@ -208,22 +212,14 @@
     let contentdisp = case partContentFilename msg of
                         Just f  -> [("Content-Disposition", "attachment;filename=\"" <> T.encodeUtf8 f <> "\"")]
                         Nothing -> []
-    let contenttype = [("Content-Type", T.encodeUtf8 $ partContentType msg)]
+    let contenttype = [("Content-Type", T.encodeUtf8 $ CI.original $ partContentType msg)]
 
     let source = rawMsg $= C.map (Chunk . fromByteString)
 
     sendWaiResponse $ ResponseSource status200 (contentdisp ++ contenttype) source
 
-
-pandocWriterOpts :: WriterOptions
-pandocWriterOpts = def
-    { writerHtml5    = True
-    , writerWrapText = False
-    }
-
-pandocReaderOpts :: ReaderOptions
-pandocReaderOpts = def
-    { readerExtensions = S.delete Ext_blank_before_blockquote $ readerExtensions def
-    , readerSmart      = True
-    , readerParseRaw   = False
-    }
+markdownSettings :: MarkdownSettings
+markdownSettings = def { msLinkNewTab = True
+                       , msXssProtect = False -- We protect using our own filtering in FilterHtml.hs
+                       , msBlankBeforeBlockquote = False
+                       }
diff --git a/src/Import.hs b/src/Import.hs
--- a/src/Import.hs
+++ b/src/Import.hs
@@ -16,29 +16,34 @@
 -}
 module Import
     ( module Prelude
-    , module Foundation
-    , module Settings
-    , module Data.Conduit
-    , module Data.Monoid
     , module Control.Applicative
     , module Control.Exception.Lifted
     , module Control.Monad
+    , module Data.Conduit
+    , module Data.List
+    , module Data.Maybe
+    , module Data.Monoid
+    , module Foundation
+    , module NotmuchCmd
+    , module Settings
     , module Yesod
     ) where
 
 #if __GLASGOW_HASKELL__ >= 706
-import Prelude hiding (writeFile, readFile, head, tail, init, last)
+import Prelude hiding (writeFile, readFile, head, init, last)
 #else
-import Prelude hiding (writeFile, readFile, head, tail, init, last, catch)
+import Prelude hiding (writeFile, readFile, head, init, last, catch)
 #endif
 
 import Foundation
-import Settings (development, production)
+import Settings
+import NotmuchCmd
 
-import Data.Conduit
-import Data.Monoid (Monoid (mappend, mempty, mconcat), (<>))
 import Control.Applicative ((<$>), (<*>), pure, (<|>))
 import Control.Exception.Lifted (catch)
-import Control.Monad (void, when)
-
-import Yesod
+import Control.Monad (void, when, forM, replicateM, unless)
+import Data.Conduit
+import Data.List (find)
+import Data.Monoid (Monoid (mappend, mempty, mconcat), (<>))
+import Data.Maybe (listToMaybe, fromMaybe, catMaybes)
+import Yesod hiding (loadConfig)
diff --git a/src/NotmuchCmd.hs b/src/NotmuchCmd.hs
--- a/src/NotmuchCmd.hs
+++ b/src/NotmuchCmd.hs
@@ -47,6 +47,7 @@
   -- * Utils
   , notmuchRaw
   , notmuchJson
+  , notmuchVersion
 ) where
 
 import Prelude
@@ -59,11 +60,14 @@
 import Data.Conduit
 import Data.Conduit.Attoparsec (sinkParser)
 import Data.Conduit.Process
+import Data.Monoid ((<>))
 import Data.Time.Calendar (Day(..))
 import Data.Time.Clock
 import Data.Time.Clock.POSIX
 import Data.Typeable (Typeable)
+import Data.Version (Version(..), parseVersion)
 import Text.Blaze (ToMarkup(..))
+import Text.ParserCombinators.ReadP (readP_to_S)
 import System.Process
 import System.Exit
 import Yesod (PathPiece)
@@ -133,8 +137,8 @@
 
 data MessagePart = MessagePart {
       partID :: Int
-    , partContentType :: T.Text
-    , partContentCharset :: Maybe T.Text
+    , partContentType :: CI.CI T.Text
+    , partContentCharset :: Maybe (CI.CI T.Text)
     , partContentFilename :: Maybe T.Text
     , partContent :: MessageContent
 } deriving (Show,Eq)
@@ -150,16 +154,16 @@
 instance FromJSON MessagePart where
     parseJSON (Object v) = do
         i <- v .: "id"
-        t <- v .: "content-type"
+        t <- CI.mk . T.toLower <$> v .: "content-type"
         x <- v .:? "content"
         f <- v .:? "filename"
-        cs <- v .:? "content-charset"
-        let ctype = T.takeWhile (/= '/') t
+        cs <- fmap CI.mk <$> v .:? "content-charset"
+        let ctype = CI.map (T.takeWhile (/= '/')) t
         case (ctype, x) of
             ("multipart", Just (Array _)) -> MessagePart i t cs f . ContentMultipart <$> v .: "content"
             ("message", Just (Array lst)) | t == "message/rfc822" -> MessagePart i t cs f <$> parseRFC822 lst
             (_, Just (String c)) -> return $ MessagePart i t cs f $ ContentText c
-            (_, Just _) -> return $ MessagePart i t cs f $ ContentText $ "Unknown content-type: " `T.append` t
+            (_, Just _) -> return $ MessagePart i t cs f $ ContentText $ "Unknown content-type: " <> CI.original t
             (_, Nothing) -> return $ MessagePart i t cs f $ ContentText ""
 
     parseJSON x = fail $ "Error parsing part: " ++ show x
@@ -286,3 +290,13 @@
     case fromJSON v of
         Error e -> throw $ NotmuchError $ "Error parsing for " ++ show args ++ " : " ++ e
         Success x -> return x
+
+-- | The version of notmuch
+notmuchVersion :: MonadIO m => m Version
+notmuchVersion = do
+    out <- liftIO $ readProcess "notmuch" ["--version"] ""
+    let vStr = words out !! 1
+    let vs = filter (\(_,r) -> r == "") $ readP_to_S parseVersion vStr
+    case vs of
+        ((v,_):_) -> return v
+        _ -> throw $ NotmuchError $ "Unable to parse version: " ++ vStr
diff --git a/src/Settings.hs b/src/Settings.hs
--- a/src/Settings.hs
+++ b/src/Settings.hs
@@ -15,52 +15,23 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
 
--- | Settings are centralized, as much as possible, into this file. This
--- includes database connection settings, static file locations, etc.
--- In addition, you can configure a number of different aspects of Yesod
--- by overriding methods in the Yesod typeclass. That instance is
--- declared in the Foundation.hs file.
 module Settings where
 
 import Prelude
+
+import Control.Applicative
 import Data.Aeson
-import Text.Shakespeare.Text (st)
-import Language.Haskell.TH.Syntax
-import Yesod.Default.Config
-import Yesod.Default.Util
-import Data.Text (Text)
-import qualified Data.Text.Encoding as TE
 import Data.ByteString (ByteString)
-import Data.Yaml
-import Control.Applicative
 import Data.Default (def)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Text.Encoding (encodeUtf8)
+import Data.Yaml
+import Language.Haskell.TH.Syntax (Q, Exp)
 import Text.Hamlet
-
--- Static setting below. Changing these requires a recompile
-
--- | The location of static files on your system. This is a file system
--- path. The default value works properly with your scaffolded site.
-staticDir :: FilePath
-staticDir = "static"
-
--- | The base URL for your static files. As you can see by the default
--- value, this can simply be "static" appended to your application root.
--- A powerful optimization can be serving static files from a separate
--- domain name. This allows you to use a web server optimized for static
--- files, more easily set expires and cache values, and avoid possibly
--- costly transference of cookies on static files. For more information,
--- please see:
---   http://code.google.com/speed/page-speed/docs/request.html#ServeFromCookielessDomain
---
--- If you change the resource pattern for StaticR in Foundation.hs, you will
--- have to make a corresponding change here.
---
--- To see how this value is used, see urlRenderOverride in Foundation.hs
-staticRoot :: AppConfig DefaultEnv x -> Text
-staticRoot conf = [st|#{appRoot conf}/static|]
+import Yesod.Default.Config
+import Yesod.Default.Util
 
--- | Settings for 'widgetFile', such as which template languages to support and
--- default Hamlet settings.
 widgetFileSettings :: WidgetFileSettings
 widgetFileSettings = def
     { wfsHamletSettings = defaultHamletSettings
@@ -68,9 +39,6 @@
         }
     }
 
--- The rest of this file contains settings which rarely need changing by a
--- user.
-
 widgetFile :: String -> Q Exp
 widgetFile = (if development then widgetFileReload
                              else widgetFileNoReload)
@@ -100,19 +68,26 @@
     { extraHashedPwd  :: ByteString
     , extraFolders :: [(Text, String)]
     , extraRetag :: [RetagEntry]
-    , extraFromAddress :: Text
+    , extraFromAddresses :: [Text]
     , extraSentBox :: Maybe FilePath
     , extraMessageIDDomain :: Text
+    , extraGoogleClientId :: Maybe Text
+    , extraSourceLink :: Text
     } deriving Show
 
 parseExtra :: DefaultEnv -> Object -> Parser Extra
-parseExtra _ o = Extra
-    <$> (TE.encodeUtf8 <$> o .: "hashed-password")
-    <*> (o .: "folders" >>= mapM parseFolder)
-    <*> o .: "retag"
-    <*> o .: "from-address"
-    <*> o .:? "sent-box"
-    <*> o .:? "message-id-domain" .!= "" 
+parseExtra _ o = do
+    pwd <- encodeUtf8 <$> o .: "hashed-password"
+    fdl <- o .: "folders" >>= mapM parseFolder
+    retag <- o .: "retag"
+    from <- o .:? "from-address" .!= "<test@example.com>"
+    fromlst <- o .:? "from-addresses"
+    let f = fromMaybe [from] fromlst
+    sent <- o .:? "sent-box"
+    dom <- o .:? "message-id-domain" .!= ""
+    gcid <- o .:? "google-client-id"
+    sl <- o .:? "source-link" .!= "https://bitbucket.org/wuzzeb/notmuch-web/src"
+    return $ Extra pwd fdl retag f sent dom gcid sl
 
 parseFolder :: Object -> Parser (Text, String)
 parseFolder o = (,) <$> o .: "name"
diff --git a/src/StaticFiles.hs b/src/StaticFiles.hs
--- a/src/StaticFiles.hs
+++ b/src/StaticFiles.hs
@@ -19,15 +19,10 @@
 import Prelude (IO)
 import Yesod.Static
 import qualified Yesod.Static as Static
-import Settings (staticDir, development)
+import Settings (development)
 
--- | use this to create your static file serving site
 staticSite :: IO Static.Static
-staticSite = if development then Static.staticDevel staticDir
-                            else Static.static      staticDir
+staticSite = if development then Static.staticDevel "static"
+                            else Static.static      "static"
 
--- | This generates easy references to files in the static directory at compile time,
---   giving you compile-time verification that referenced files exist.
---   Warning: any files added to your static directory during run-time can't be
---   accessed this way. You'll have to use their FilePath or URL to access them.
-$(staticFiles Settings.staticDir)
+$(staticFiles "static")
diff --git a/static/js/jquery-pjax-1-7-0.js b/static/js/jquery-pjax-1-7-0.js
deleted file mode 100644
--- a/static/js/jquery-pjax-1-7-0.js
+++ /dev/null
@@ -1,822 +0,0 @@
-// jquery.pjax.js
-// copyright chris wanstrath
-// https://github.com/defunkt/jquery-pjax
-
-(function($){
-
-// When called on a container with a selector, fetches the href with
-// ajax into the container or with the data-pjax attribute on the link
-// itself.
-//
-// Tries to make sure the back button and ctrl+click work the way
-// you'd expect.
-//
-// Exported as $.fn.pjax
-//
-// Accepts a jQuery ajax options object that may include these
-// pjax specific options:
-//
-//
-// container - Where to stick the response body. Usually a String selector.
-//             $(container).html(xhr.responseBody)
-//             (default: current jquery context)
-//      push - Whether to pushState the URL. Defaults to true (of course).
-//   replace - Want to use replaceState instead? That's cool.
-//
-// For convenience the second parameter can be either the container or
-// the options object.
-//
-// Returns the jQuery object
-function fnPjax(selector, container, options) {
-  var context = this
-  return this.on('click.pjax', selector, function(event) {
-    var opts = $.extend({}, optionsFor(container, options))
-    if (!opts.container)
-      opts.container = $(this).attr('data-pjax') || context
-    handleClick(event, opts)
-  })
-}
-
-// Public: pjax on click handler
-//
-// Exported as $.pjax.click.
-//
-// event   - "click" jQuery.Event
-// options - pjax options
-//
-// Examples
-//
-//   $(document).on('click', 'a', $.pjax.click)
-//   // is the same as
-//   $(document).pjax('a')
-//
-//  $(document).on('click', 'a', function(event) {
-//    var container = $(this).closest('[data-pjax-container]')
-//    $.pjax.click(event, container)
-//  })
-//
-// Returns nothing.
-function handleClick(event, container, options) {
-  options = optionsFor(container, options)
-
-  var link = event.currentTarget
-
-  if (link.tagName.toUpperCase() !== 'A')
-    throw "$.fn.pjax or $.pjax.click requires an anchor element"
-
-  // Middle click, cmd click, and ctrl click should open
-  // links in a new tab as normal.
-  if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
-    return
-
-  // Ignore cross origin links
-  if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
-    return
-
-  // Ignore anchors on the same page
-  if (link.hash && link.href.replace(link.hash, '') ===
-       location.href.replace(location.hash, ''))
-    return
-
-  // Ignore empty anchor "foo.html#"
-  if (link.href === location.href + '#')
-    return
-
-  var defaults = {
-    url: link.href,
-    container: $(link).attr('data-pjax'),
-    target: link,
-    fragment: null
-  }
-
-  var opts = $.extend({}, defaults, options)
-  var clickEvent = $.Event('pjax:click')
-  $(link).trigger(clickEvent, [opts])
-
-  if (!clickEvent.isDefaultPrevented()) {
-    pjax(opts)
-    event.preventDefault()
-  }
-}
-
-// Public: pjax on form submit handler
-//
-// Exported as $.pjax.submit
-//
-// event   - "click" jQuery.Event
-// options - pjax options
-//
-// Examples
-//
-//  $(document).on('submit', 'form', function(event) {
-//    var container = $(this).closest('[data-pjax-container]')
-//    $.pjax.submit(event, container)
-//  })
-//
-// Returns nothing.
-function handleSubmit(event, container, options) {
-  options = optionsFor(container, options)
-
-  var form = event.currentTarget
-
-  if (form.tagName.toUpperCase() !== 'FORM')
-    throw "$.pjax.submit requires a form element"
-
-  var defaults = {
-    type: form.method.toUpperCase(),
-    url: form.action,
-    data: $(form).serializeArray(),
-    container: $(form).attr('data-pjax'),
-    target: form,
-    fragment: null
-  }
-
-  pjax($.extend({}, defaults, options))
-
-  event.preventDefault()
-}
-
-// Loads a URL with ajax, puts the response body inside a container,
-// then pushState()'s the loaded URL.
-//
-// Works just like $.ajax in that it accepts a jQuery ajax
-// settings object (with keys like url, type, data, etc).
-//
-// Accepts these extra keys:
-//
-// container - Where to stick the response body.
-//             $(container).html(xhr.responseBody)
-//      push - Whether to pushState the URL. Defaults to true (of course).
-//   replace - Want to use replaceState instead? That's cool.
-//
-// Use it just like $.ajax:
-//
-//   var xhr = $.pjax({ url: this.href, container: '#main' })
-//   console.log( xhr.readyState )
-//
-// Returns whatever $.ajax returns.
-function pjax(options) {
-  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
-
-  if ($.isFunction(options.url)) {
-    options.url = options.url()
-  }
-
-  var target = options.target
-
-  var hash = parseURL(options.url).hash
-
-  var context = options.context = findContainerFor(options.container)
-
-  // We want the browser to maintain two separate internal caches: one
-  // for pjax'd partial page loads and one for normal page loads.
-  // Without adding this secret parameter, some browsers will often
-  // confuse the two.
-  if (!options.data) options.data = {}
-  options.data._pjax = context.selector
-
-  function fire(type, args) {
-    var event = $.Event(type, { relatedTarget: target })
-    context.trigger(event, args)
-    return !event.isDefaultPrevented()
-  }
-
-  var timeoutTimer
-
-  options.beforeSend = function(xhr, settings) {
-    // No timeout for non-GET requests
-    // Its not safe to request the resource again with a fallback method.
-    if (settings.type !== 'GET') {
-      settings.timeout = 0
-    }
-
-    xhr.setRequestHeader('X-PJAX', 'true')
-    xhr.setRequestHeader('X-PJAX-Container', context.selector)
-
-    if (!fire('pjax:beforeSend', [xhr, settings]))
-      return false
-
-    if (settings.timeout > 0) {
-      timeoutTimer = setTimeout(function() {
-        if (fire('pjax:timeout', [xhr, options]))
-          xhr.abort('timeout')
-      }, settings.timeout)
-
-      // Clear timeout setting so jquerys internal timeout isn't invoked
-      settings.timeout = 0
-    }
-
-    options.requestUrl = parseURL(settings.url).href
-  }
-
-  options.complete = function(xhr, textStatus) {
-    if (timeoutTimer)
-      clearTimeout(timeoutTimer)
-
-    fire('pjax:complete', [xhr, textStatus, options])
-
-    fire('pjax:end', [xhr, options])
-  }
-
-  options.error = function(xhr, textStatus, errorThrown) {
-    var container = extractContainer("", xhr, options)
-
-    var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
-    if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
-      locationReplace(container.url)
-    }
-  }
-
-  options.success = function(data, status, xhr) {
-    // If $.pjax.defaults.version is a function, invoke it first.
-    // Otherwise it can be a static string.
-    var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
-      $.pjax.defaults.version() :
-      $.pjax.defaults.version
-
-    var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
-
-    var container = extractContainer(data, xhr, options)
-
-    // If there is a layout version mismatch, hard load the new url
-    if (currentVersion && latestVersion && currentVersion !== latestVersion) {
-      locationReplace(container.url)
-      return
-    }
-
-    // If the new response is missing a body, hard load the page
-    if (!container.contents) {
-      locationReplace(container.url)
-      return
-    }
-
-    pjax.state = {
-      id: options.id || uniqueId(),
-      url: container.url,
-      title: container.title,
-      container: context.selector,
-      fragment: options.fragment,
-      timeout: options.timeout
-    }
-
-    if (options.push || options.replace) {
-      window.history.replaceState(pjax.state, container.title, container.url)
-    }
-
-    if (container.title) document.title = container.title
-    context.html(container.contents)
-    executeScriptTags(container.scripts)
-
-    // Scroll to top by default
-    if (typeof options.scrollTo === 'number')
-      $(window).scrollTop(options.scrollTo)
-
-    // If the URL has a hash in it, make sure the browser
-    // knows to navigate to the hash.
-    if ( hash !== '' ) {
-      // Avoid using simple hash set here. Will add another history
-      // entry. Replace the url with replaceState and scroll to target
-      // by hand.
-      //
-      //   window.location.hash = hash
-      var url = parseURL(container.url)
-      url.hash = hash
-
-      pjax.state.url = url.href
-      window.history.replaceState(pjax.state, container.title, url.href)
-
-      var target = $(url.hash)
-      if (target.length) $(window).scrollTop(target.offset().top)
-    }
-
-    fire('pjax:success', [data, status, xhr, options])
-  }
-
-
-  // Initialize pjax.state for the initial page load. Assume we're
-  // using the container and options of the link we're loading for the
-  // back button to the initial page. This ensures good back button
-  // behavior.
-  if (!pjax.state) {
-    pjax.state = {
-      id: uniqueId(),
-      url: window.location.href,
-      title: document.title,
-      container: context.selector,
-      fragment: options.fragment,
-      timeout: options.timeout
-    }
-    window.history.replaceState(pjax.state, document.title)
-  }
-
-  // Cancel the current request if we're already pjaxing
-  var xhr = pjax.xhr
-  if ( xhr && xhr.readyState < 4) {
-    xhr.onreadystatechange = $.noop
-    xhr.abort()
-  }
-
-  pjax.options = options
-  var xhr = pjax.xhr = $.ajax(options)
-
-  if (xhr.readyState > 0) {
-    if (options.push && !options.replace) {
-      // Cache current container element before replacing it
-      cachePush(pjax.state.id, context.clone().contents())
-
-      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
-    }
-
-    fire('pjax:start', [xhr, options])
-    fire('pjax:send', [xhr, options])
-  }
-
-  return pjax.xhr
-}
-
-// Public: Reload current page with pjax.
-//
-// Returns whatever $.pjax returns.
-function pjaxReload(container, options) {
-  var defaults = {
-    url: window.location.href,
-    push: false,
-    replace: true,
-    scrollTo: false
-  }
-
-  return pjax($.extend(defaults, optionsFor(container, options)))
-}
-
-// Internal: Hard replace current state with url.
-//
-// Work for around WebKit
-//   https://bugs.webkit.org/show_bug.cgi?id=93506
-//
-// Returns nothing.
-function locationReplace(url) {
-  window.history.replaceState(null, "", "#")
-  window.location.replace(url)
-}
-
-
-var initialPop = true
-var initialURL = window.location.href
-var initialState = window.history.state
-
-// Initialize $.pjax.state if possible
-// Happens when reloading a page and coming forward from a different
-// session history.
-if (initialState && initialState.container) {
-  pjax.state = initialState
-}
-
-// Non-webkit browsers don't fire an initial popstate event
-if ('state' in window.history) {
-  initialPop = false
-}
-
-// popstate handler takes care of the back and forward buttons
-//
-// You probably shouldn't use pjax on pages with other pushState
-// stuff yet.
-function onPjaxPopstate(event) {
-  var state = event.state
-
-  if (state && state.container) {
-    // When coming forward from a seperate history session, will get an
-    // initial pop with a state we are already at. Skip reloading the current
-    // page.
-    if (initialPop && initialURL == state.url) return
-
-    var container = $(state.container)
-    if (container.length) {
-      var direction, contents = cacheMapping[state.id]
-
-      if (pjax.state) {
-        // Since state ids always increase, we can deduce the history
-        // direction from the previous state.
-        direction = pjax.state.id < state.id ? 'forward' : 'back'
-
-        // Cache current container before replacement and inform the
-        // cache which direction the history shifted.
-        cachePop(direction, pjax.state.id, container.clone().contents())
-      }
-
-      var popstateEvent = $.Event('pjax:popstate', {
-        state: state,
-        direction: direction
-      })
-      container.trigger(popstateEvent)
-
-      var options = {
-        id: state.id,
-        url: state.url,
-        container: container,
-        push: false,
-        fragment: state.fragment,
-        timeout: state.timeout,
-        scrollTo: false
-      }
-
-      if (contents) {
-        container.trigger('pjax:start', [null, options])
-
-        if (state.title) document.title = state.title
-        container.html(contents)
-        pjax.state = state
-
-        container.trigger('pjax:end', [null, options])
-      } else {
-        pjax(options)
-      }
-
-      // Force reflow/relayout before the browser tries to restore the
-      // scroll position.
-      container[0].offsetHeight
-    } else {
-      locationReplace(location.href)
-    }
-  }
-  initialPop = false
-}
-
-// Fallback version of main pjax function for browsers that don't
-// support pushState.
-//
-// Returns nothing since it retriggers a hard form submission.
-function fallbackPjax(options) {
-  var url = $.isFunction(options.url) ? options.url() : options.url,
-      method = options.type ? options.type.toUpperCase() : 'GET'
-
-  var form = $('<form>', {
-    method: method === 'GET' ? 'GET' : 'POST',
-    action: url,
-    style: 'display:none'
-  })
-
-  if (method !== 'GET' && method !== 'POST') {
-    form.append($('<input>', {
-      type: 'hidden',
-      name: '_method',
-      value: method.toLowerCase()
-    }))
-  }
-
-  var data = options.data
-  if (typeof data === 'string') {
-    $.each(data.split('&'), function(index, value) {
-      var pair = value.split('=')
-      form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
-    })
-  } else if (typeof data === 'object') {
-    for (key in data)
-      form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
-  }
-
-  $(document.body).append(form)
-  form.submit()
-}
-
-// Internal: Generate unique id for state object.
-//
-// Use a timestamp instead of a counter since ids should still be
-// unique across page loads.
-//
-// Returns Number.
-function uniqueId() {
-  return (new Date).getTime()
-}
-
-// Internal: Strips _pjax param from url
-//
-// url - String
-//
-// Returns String.
-function stripPjaxParam(url) {
-  return url
-    .replace(/\?_pjax=[^&]+&?/, '?')
-    .replace(/_pjax=[^&]+&?/, '')
-    .replace(/[\?&]$/, '')
-}
-
-// Internal: Parse URL components and returns a Locationish object.
-//
-// url - String URL
-//
-// Returns HTMLAnchorElement that acts like Location.
-function parseURL(url) {
-  var a = document.createElement('a')
-  a.href = url
-  return a
-}
-
-// Internal: Build options Object for arguments.
-//
-// For convenience the first parameter can be either the container or
-// the options object.
-//
-// Examples
-//
-//   optionsFor('#container')
-//   // => {container: '#container'}
-//
-//   optionsFor('#container', {push: true})
-//   // => {container: '#container', push: true}
-//
-//   optionsFor({container: '#container', push: true})
-//   // => {container: '#container', push: true}
-//
-// Returns options Object.
-function optionsFor(container, options) {
-  // Both container and options
-  if ( container && options )
-    options.container = container
-
-  // First argument is options Object
-  else if ( $.isPlainObject(container) )
-    options = container
-
-  // Only container
-  else
-    options = {container: container}
-
-  // Find and validate container
-  if (options.container)
-    options.container = findContainerFor(options.container)
-
-  return options
-}
-
-// Internal: Find container element for a variety of inputs.
-//
-// Because we can't persist elements using the history API, we must be
-// able to find a String selector that will consistently find the Element.
-//
-// container - A selector String, jQuery object, or DOM Element.
-//
-// Returns a jQuery object whose context is `document` and has a selector.
-function findContainerFor(container) {
-  container = $(container)
-
-  if ( !container.length ) {
-    throw "no pjax container for " + container.selector
-  } else if ( container.selector !== '' && container.context === document ) {
-    return container
-  } else if ( container.attr('id') ) {
-    return $('#' + container.attr('id'))
-  } else {
-    throw "cant get selector for pjax container!"
-  }
-}
-
-// Internal: Filter and find all elements matching the selector.
-//
-// Where $.fn.find only matches descendants, findAll will test all the
-// top level elements in the jQuery object as well.
-//
-// elems    - jQuery object of Elements
-// selector - String selector to match
-//
-// Returns a jQuery object.
-function findAll(elems, selector) {
-  return elems.filter(selector).add(elems.find(selector));
-}
-
-function parseHTML(html) {
-  return $.parseHTML(html, document, true)
-}
-
-// Internal: Extracts container and metadata from response.
-//
-// 1. Extracts X-PJAX-URL header if set
-// 2. Extracts inline <title> tags
-// 3. Builds response Element and extracts fragment if set
-//
-// data    - String response data
-// xhr     - XHR response
-// options - pjax options Object
-//
-// Returns an Object with url, title, and contents keys.
-function extractContainer(data, xhr, options) {
-  var obj = {}
-
-  // Prefer X-PJAX-URL header if it was set, otherwise fallback to
-  // using the original requested url.
-  obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
-
-  // Attempt to parse response html into elements
-  if (/<html/i.test(data)) {
-    var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
-    var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
-  } else {
-    var $head = $body = $(parseHTML(data))
-  }
-
-  // If response data is empty, return fast
-  if ($body.length === 0)
-    return obj
-
-  // If there's a <title> tag in the header, use it as
-  // the page's title.
-  obj.title = findAll($head, 'title').last().text()
-
-  if (options.fragment) {
-    // If they specified a fragment, look for it in the response
-    // and pull it out.
-    if (options.fragment === 'body') {
-      var $fragment = $body
-    } else {
-      var $fragment = findAll($body, options.fragment).first()
-    }
-
-    if ($fragment.length) {
-      obj.contents = $fragment.contents()
-
-      // If there's no title, look for data-title and title attributes
-      // on the fragment
-      if (!obj.title)
-        obj.title = $fragment.attr('title') || $fragment.data('title')
-    }
-
-  } else if (!/<html/i.test(data)) {
-    obj.contents = $body
-  }
-
-  // Clean up any <title> tags
-  if (obj.contents) {
-    // Remove any parent title elements
-    obj.contents = obj.contents.not(function() { return $(this).is('title') })
-
-    // Then scrub any titles from their descendents
-    obj.contents.find('title').remove()
-
-    // Gather all script[src] elements
-    obj.scripts = findAll(obj.contents, 'script[src]').remove()
-    obj.contents = obj.contents.not(obj.scripts)
-  }
-
-  // Trim any whitespace off the title
-  if (obj.title) obj.title = $.trim(obj.title)
-
-  return obj
-}
-
-// Load an execute scripts using standard script request.
-//
-// Avoids jQuery's traditional $.getScript which does a XHR request and
-// globalEval.
-//
-// scripts - jQuery object of script Elements
-//
-// Returns nothing.
-function executeScriptTags(scripts) {
-  if (!scripts) return
-
-  var existingScripts = $('script[src]')
-
-  scripts.each(function() {
-    var src = this.src
-    var matchedScripts = existingScripts.filter(function() {
-      return this.src === src
-    })
-    if (matchedScripts.length) return
-
-    var script = document.createElement('script')
-    script.type = $(this).attr('type')
-    script.src = $(this).attr('src')
-    document.head.appendChild(script)
-  })
-}
-
-// Internal: History DOM caching class.
-var cacheMapping      = {}
-var cacheForwardStack = []
-var cacheBackStack    = []
-
-// Push previous state id and container contents into the history
-// cache. Should be called in conjunction with `pushState` to save the
-// previous container contents.
-//
-// id    - State ID Number
-// value - DOM Element to cache
-//
-// Returns nothing.
-function cachePush(id, value) {
-  cacheMapping[id] = value
-  cacheBackStack.push(id)
-
-  // Remove all entires in forward history stack after pushing
-  // a new page.
-  while (cacheForwardStack.length)
-    delete cacheMapping[cacheForwardStack.shift()]
-
-  // Trim back history stack to max cache length.
-  while (cacheBackStack.length > pjax.defaults.maxCacheLength)
-    delete cacheMapping[cacheBackStack.shift()]
-}
-
-// Shifts cache from directional history cache. Should be
-// called on `popstate` with the previous state id and container
-// contents.
-//
-// direction - "forward" or "back" String
-// id        - State ID Number
-// value     - DOM Element to cache
-//
-// Returns nothing.
-function cachePop(direction, id, value) {
-  var pushStack, popStack
-  cacheMapping[id] = value
-
-  if (direction === 'forward') {
-    pushStack = cacheBackStack
-    popStack  = cacheForwardStack
-  } else {
-    pushStack = cacheForwardStack
-    popStack  = cacheBackStack
-  }
-
-  pushStack.push(id)
-  if (id = popStack.pop())
-    delete cacheMapping[id]
-}
-
-// Public: Find version identifier for the initial page load.
-//
-// Returns String version or undefined.
-function findVersion() {
-  return $('meta').filter(function() {
-    var name = $(this).attr('http-equiv')
-    return name && name.toUpperCase() === 'X-PJAX-VERSION'
-  }).attr('content')
-}
-
-// Install pjax functions on $.pjax to enable pushState behavior.
-//
-// Does nothing if already enabled.
-//
-// Examples
-//
-//     $.pjax.enable()
-//
-// Returns nothing.
-function enable() {
-  $.fn.pjax = fnPjax
-  $.pjax = pjax
-  $.pjax.enable = $.noop
-  $.pjax.disable = disable
-  $.pjax.click = handleClick
-  $.pjax.submit = handleSubmit
-  $.pjax.reload = pjaxReload
-  $.pjax.defaults = {
-    timeout: 650,
-    push: true,
-    replace: false,
-    type: 'GET',
-    dataType: 'html',
-    scrollTo: 0,
-    maxCacheLength: 20,
-    version: findVersion
-  }
-  $(window).on('popstate.pjax', onPjaxPopstate)
-}
-
-// Disable pushState behavior.
-//
-// This is the case when a browser doesn't support pushState. It is
-// sometimes useful to disable pushState for debugging on a modern
-// browser.
-//
-// Examples
-//
-//     $.pjax.disable()
-//
-// Returns nothing.
-function disable() {
-  $.fn.pjax = function() { return this }
-  $.pjax = fallbackPjax
-  $.pjax.enable = enable
-  $.pjax.disable = $.noop
-  $.pjax.click = $.noop
-  $.pjax.submit = $.noop
-  $.pjax.reload = function() { window.location.reload() }
-
-  $(window).off('popstate.pjax', onPjaxPopstate)
-}
-
-
-// Add the state property to jQuery's event object so we can use it in
-// $(window).bind('popstate')
-if ( $.inArray('state', $.event.props) < 0 )
-  $.event.props.push('state')
-
- // Is pjax supported by this browser?
-$.support.pjax =
-  window.history && window.history.pushState && window.history.replaceState &&
-  // pushState isn't reliable on iOS until 5.
-  !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
-
-$.support.pjax ? enable() : disable()
-
-})(jQuery);
diff --git a/static/js/jquery-pjax-1-7-3.js b/static/js/jquery-pjax-1-7-3.js
new file mode 100644
--- /dev/null
+++ b/static/js/jquery-pjax-1-7-3.js
@@ -0,0 +1,838 @@
+// jquery.pjax.js
+// copyright chris wanstrath
+// https://github.com/defunkt/jquery-pjax
+
+(function($){
+
+// When called on a container with a selector, fetches the href with
+// ajax into the container or with the data-pjax attribute on the link
+// itself.
+//
+// Tries to make sure the back button and ctrl+click work the way
+// you'd expect.
+//
+// Exported as $.fn.pjax
+//
+// Accepts a jQuery ajax options object that may include these
+// pjax specific options:
+//
+//
+// container - Where to stick the response body. Usually a String selector.
+//             $(container).html(xhr.responseBody)
+//             (default: current jquery context)
+//      push - Whether to pushState the URL. Defaults to true (of course).
+//   replace - Want to use replaceState instead? That's cool.
+//
+// For convenience the second parameter can be either the container or
+// the options object.
+//
+// Returns the jQuery object
+function fnPjax(selector, container, options) {
+  var context = this
+  return this.on('click.pjax', selector, function(event) {
+    var opts = $.extend({}, optionsFor(container, options))
+    if (!opts.container)
+      opts.container = $(this).attr('data-pjax') || context
+    handleClick(event, opts)
+  })
+}
+
+// Public: pjax on click handler
+//
+// Exported as $.pjax.click.
+//
+// event   - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+//   $(document).on('click', 'a', $.pjax.click)
+//   // is the same as
+//   $(document).pjax('a')
+//
+//  $(document).on('click', 'a', function(event) {
+//    var container = $(this).closest('[data-pjax-container]')
+//    $.pjax.click(event, container)
+//  })
+//
+// Returns nothing.
+function handleClick(event, container, options) {
+  options = optionsFor(container, options)
+
+  var link = event.currentTarget
+
+  if (link.tagName.toUpperCase() !== 'A')
+    throw "$.fn.pjax or $.pjax.click requires an anchor element"
+
+  // Middle click, cmd click, and ctrl click should open
+  // links in a new tab as normal.
+  if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
+    return
+
+  // Ignore cross origin links
+  if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
+    return
+
+  // Ignore anchors on the same page
+  if (link.hash && link.href.replace(link.hash, '') ===
+       location.href.replace(location.hash, ''))
+    return
+
+  // Ignore empty anchor "foo.html#"
+  if (link.href === location.href + '#')
+    return
+
+  var defaults = {
+    url: link.href,
+    container: $(link).attr('data-pjax'),
+    target: link
+  }
+
+  var opts = $.extend({}, defaults, options)
+  var clickEvent = $.Event('pjax:click')
+  $(link).trigger(clickEvent, [opts])
+
+  if (!clickEvent.isDefaultPrevented()) {
+    pjax(opts)
+    event.preventDefault()
+  }
+}
+
+// Public: pjax on form submit handler
+//
+// Exported as $.pjax.submit
+//
+// event   - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+//  $(document).on('submit', 'form', function(event) {
+//    var container = $(this).closest('[data-pjax-container]')
+//    $.pjax.submit(event, container)
+//  })
+//
+// Returns nothing.
+function handleSubmit(event, container, options) {
+  options = optionsFor(container, options)
+
+  var form = event.currentTarget
+
+  if (form.tagName.toUpperCase() !== 'FORM')
+    throw "$.pjax.submit requires a form element"
+
+  var defaults = {
+    type: form.method.toUpperCase(),
+    url: form.action,
+    data: $(form).serializeArray(),
+    container: $(form).attr('data-pjax'),
+    target: form
+  }
+
+  pjax($.extend({}, defaults, options))
+
+  event.preventDefault()
+}
+
+// Loads a URL with ajax, puts the response body inside a container,
+// then pushState()'s the loaded URL.
+//
+// Works just like $.ajax in that it accepts a jQuery ajax
+// settings object (with keys like url, type, data, etc).
+//
+// Accepts these extra keys:
+//
+// container - Where to stick the response body.
+//             $(container).html(xhr.responseBody)
+//      push - Whether to pushState the URL. Defaults to true (of course).
+//   replace - Want to use replaceState instead? That's cool.
+//
+// Use it just like $.ajax:
+//
+//   var xhr = $.pjax({ url: this.href, container: '#main' })
+//   console.log( xhr.readyState )
+//
+// Returns whatever $.ajax returns.
+function pjax(options) {
+  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
+
+  if ($.isFunction(options.url)) {
+    options.url = options.url()
+  }
+
+  var target = options.target
+
+  var hash = parseURL(options.url).hash
+
+  var context = options.context = findContainerFor(options.container)
+
+  // We want the browser to maintain two separate internal caches: one
+  // for pjax'd partial page loads and one for normal page loads.
+  // Without adding this secret parameter, some browsers will often
+  // confuse the two.
+  if (!options.data) options.data = {}
+  options.data._pjax = context.selector
+
+  function fire(type, args) {
+    var event = $.Event(type, { relatedTarget: target })
+    context.trigger(event, args)
+    return !event.isDefaultPrevented()
+  }
+
+  var timeoutTimer
+
+  options.beforeSend = function(xhr, settings) {
+    // No timeout for non-GET requests
+    // Its not safe to request the resource again with a fallback method.
+    if (settings.type !== 'GET') {
+      settings.timeout = 0
+    }
+
+    xhr.setRequestHeader('X-PJAX', 'true')
+    xhr.setRequestHeader('X-PJAX-Container', context.selector)
+
+    if (!fire('pjax:beforeSend', [xhr, settings]))
+      return false
+
+    if (settings.timeout > 0) {
+      timeoutTimer = setTimeout(function() {
+        if (fire('pjax:timeout', [xhr, options]))
+          xhr.abort('timeout')
+      }, settings.timeout)
+
+      // Clear timeout setting so jquerys internal timeout isn't invoked
+      settings.timeout = 0
+    }
+
+    options.requestUrl = parseURL(settings.url).href
+  }
+
+  options.complete = function(xhr, textStatus) {
+    if (timeoutTimer)
+      clearTimeout(timeoutTimer)
+
+    fire('pjax:complete', [xhr, textStatus, options])
+
+    fire('pjax:end', [xhr, options])
+  }
+
+  options.error = function(xhr, textStatus, errorThrown) {
+    var container = extractContainer("", xhr, options)
+
+    var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
+    if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
+      locationReplace(container.url)
+    }
+  }
+
+  options.success = function(data, status, xhr) {
+    // If $.pjax.defaults.version is a function, invoke it first.
+    // Otherwise it can be a static string.
+    var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
+      $.pjax.defaults.version() :
+      $.pjax.defaults.version
+
+    var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
+
+    var container = extractContainer(data, xhr, options)
+
+    // If there is a layout version mismatch, hard load the new url
+    if (currentVersion && latestVersion && currentVersion !== latestVersion) {
+      locationReplace(container.url)
+      return
+    }
+
+    // If the new response is missing a body, hard load the page
+    if (!container.contents) {
+      locationReplace(container.url)
+      return
+    }
+
+    pjax.state = {
+      id: options.id || uniqueId(),
+      url: container.url,
+      title: container.title,
+      container: context.selector,
+      fragment: options.fragment,
+      timeout: options.timeout
+    }
+
+    if (options.push || options.replace) {
+      window.history.replaceState(pjax.state, container.title, container.url)
+    }
+
+    // Clear out any focused controls before inserting new page contents.
+    document.activeElement.blur()
+
+    if (container.title) document.title = container.title
+    context.html(container.contents)
+
+    // FF bug: Won't autofocus fields that are inserted via JS.
+    // This behavior is incorrect. So if theres no current focus, autofocus
+    // the last field.
+    //
+    // http://www.w3.org/html/wg/drafts/html/master/forms.html
+    var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
+    if (autofocusEl && document.activeElement !== autofocusEl) {
+      autofocusEl.focus();
+    }
+
+    executeScriptTags(container.scripts)
+
+    // Scroll to top by default
+    if (typeof options.scrollTo === 'number')
+      $(window).scrollTop(options.scrollTo)
+
+    // If the URL has a hash in it, make sure the browser
+    // knows to navigate to the hash.
+    if ( hash !== '' ) {
+      // Avoid using simple hash set here. Will add another history
+      // entry. Replace the url with replaceState and scroll to target
+      // by hand.
+      //
+      //   window.location.hash = hash
+      var url = parseURL(container.url)
+      url.hash = hash
+
+      pjax.state.url = url.href
+      window.history.replaceState(pjax.state, container.title, url.href)
+
+      var target = $(url.hash)
+      if (target.length) $(window).scrollTop(target.offset().top)
+    }
+
+    fire('pjax:success', [data, status, xhr, options])
+  }
+
+
+  // Initialize pjax.state for the initial page load. Assume we're
+  // using the container and options of the link we're loading for the
+  // back button to the initial page. This ensures good back button
+  // behavior.
+  if (!pjax.state) {
+    pjax.state = {
+      id: uniqueId(),
+      url: window.location.href,
+      title: document.title,
+      container: context.selector,
+      fragment: options.fragment,
+      timeout: options.timeout
+    }
+    window.history.replaceState(pjax.state, document.title)
+  }
+
+  // Cancel the current request if we're already pjaxing
+  var xhr = pjax.xhr
+  if ( xhr && xhr.readyState < 4) {
+    xhr.onreadystatechange = $.noop
+    xhr.abort()
+  }
+
+  pjax.options = options
+  var xhr = pjax.xhr = $.ajax(options)
+
+  if (xhr.readyState > 0) {
+    if (options.push && !options.replace) {
+      // Cache current container element before replacing it
+      cachePush(pjax.state.id, context.clone().contents())
+
+      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
+    }
+
+    fire('pjax:start', [xhr, options])
+    fire('pjax:send', [xhr, options])
+  }
+
+  return pjax.xhr
+}
+
+// Public: Reload current page with pjax.
+//
+// Returns whatever $.pjax returns.
+function pjaxReload(container, options) {
+  var defaults = {
+    url: window.location.href,
+    push: false,
+    replace: true,
+    scrollTo: false
+  }
+
+  return pjax($.extend(defaults, optionsFor(container, options)))
+}
+
+// Internal: Hard replace current state with url.
+//
+// Work for around WebKit
+//   https://bugs.webkit.org/show_bug.cgi?id=93506
+//
+// Returns nothing.
+function locationReplace(url) {
+  window.history.replaceState(null, "", "#")
+  window.location.replace(url)
+}
+
+
+var initialPop = true
+var initialURL = window.location.href
+var initialState = window.history.state
+
+// Initialize $.pjax.state if possible
+// Happens when reloading a page and coming forward from a different
+// session history.
+if (initialState && initialState.container) {
+  pjax.state = initialState
+}
+
+// Non-webkit browsers don't fire an initial popstate event
+if ('state' in window.history) {
+  initialPop = false
+}
+
+// popstate handler takes care of the back and forward buttons
+//
+// You probably shouldn't use pjax on pages with other pushState
+// stuff yet.
+function onPjaxPopstate(event) {
+  var state = event.state
+
+  if (state && state.container) {
+    // When coming forward from a separate history session, will get an
+    // initial pop with a state we are already at. Skip reloading the current
+    // page.
+    if (initialPop && initialURL == state.url) return
+
+    // If popping back to the same state, just skip.
+    // Could be clicking back from hashchange rather than a pushState.
+    if (pjax.state.id === state.id) return
+
+    var container = $(state.container)
+    if (container.length) {
+      var direction, contents = cacheMapping[state.id]
+
+      if (pjax.state) {
+        // Since state ids always increase, we can deduce the history
+        // direction from the previous state.
+        direction = pjax.state.id < state.id ? 'forward' : 'back'
+
+        // Cache current container before replacement and inform the
+        // cache which direction the history shifted.
+        cachePop(direction, pjax.state.id, container.clone().contents())
+      }
+
+      var popstateEvent = $.Event('pjax:popstate', {
+        state: state,
+        direction: direction
+      })
+      container.trigger(popstateEvent)
+
+      var options = {
+        id: state.id,
+        url: state.url,
+        container: container,
+        push: false,
+        fragment: state.fragment,
+        timeout: state.timeout,
+        scrollTo: false
+      }
+
+      if (contents) {
+        container.trigger('pjax:start', [null, options])
+
+        if (state.title) document.title = state.title
+        container.html(contents)
+        pjax.state = state
+
+        container.trigger('pjax:end', [null, options])
+      } else {
+        pjax(options)
+      }
+
+      // Force reflow/relayout before the browser tries to restore the
+      // scroll position.
+      container[0].offsetHeight
+    } else {
+      locationReplace(location.href)
+    }
+  }
+  initialPop = false
+}
+
+// Fallback version of main pjax function for browsers that don't
+// support pushState.
+//
+// Returns nothing since it retriggers a hard form submission.
+function fallbackPjax(options) {
+  var url = $.isFunction(options.url) ? options.url() : options.url,
+      method = options.type ? options.type.toUpperCase() : 'GET'
+
+  var form = $('<form>', {
+    method: method === 'GET' ? 'GET' : 'POST',
+    action: url,
+    style: 'display:none'
+  })
+
+  if (method !== 'GET' && method !== 'POST') {
+    form.append($('<input>', {
+      type: 'hidden',
+      name: '_method',
+      value: method.toLowerCase()
+    }))
+  }
+
+  var data = options.data
+  if (typeof data === 'string') {
+    $.each(data.split('&'), function(index, value) {
+      var pair = value.split('=')
+      form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
+    })
+  } else if (typeof data === 'object') {
+    for (key in data)
+      form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
+  }
+
+  $(document.body).append(form)
+  form.submit()
+}
+
+// Internal: Generate unique id for state object.
+//
+// Use a timestamp instead of a counter since ids should still be
+// unique across page loads.
+//
+// Returns Number.
+function uniqueId() {
+  return (new Date).getTime()
+}
+
+// Internal: Strips _pjax param from url
+//
+// url - String
+//
+// Returns String.
+function stripPjaxParam(url) {
+  return url
+    .replace(/\?_pjax=[^&]+&?/, '?')
+    .replace(/_pjax=[^&]+&?/, '')
+    .replace(/[\?&]$/, '')
+}
+
+// Internal: Parse URL components and returns a Locationish object.
+//
+// url - String URL
+//
+// Returns HTMLAnchorElement that acts like Location.
+function parseURL(url) {
+  var a = document.createElement('a')
+  a.href = url
+  return a
+}
+
+// Internal: Build options Object for arguments.
+//
+// For convenience the first parameter can be either the container or
+// the options object.
+//
+// Examples
+//
+//   optionsFor('#container')
+//   // => {container: '#container'}
+//
+//   optionsFor('#container', {push: true})
+//   // => {container: '#container', push: true}
+//
+//   optionsFor({container: '#container', push: true})
+//   // => {container: '#container', push: true}
+//
+// Returns options Object.
+function optionsFor(container, options) {
+  // Both container and options
+  if ( container && options )
+    options.container = container
+
+  // First argument is options Object
+  else if ( $.isPlainObject(container) )
+    options = container
+
+  // Only container
+  else
+    options = {container: container}
+
+  // Find and validate container
+  if (options.container)
+    options.container = findContainerFor(options.container)
+
+  return options
+}
+
+// Internal: Find container element for a variety of inputs.
+//
+// Because we can't persist elements using the history API, we must be
+// able to find a String selector that will consistently find the Element.
+//
+// container - A selector String, jQuery object, or DOM Element.
+//
+// Returns a jQuery object whose context is `document` and has a selector.
+function findContainerFor(container) {
+  container = $(container)
+
+  if ( !container.length ) {
+    throw "no pjax container for " + container.selector
+  } else if ( container.selector !== '' && container.context === document ) {
+    return container
+  } else if ( container.attr('id') ) {
+    return $('#' + container.attr('id'))
+  } else {
+    throw "cant get selector for pjax container!"
+  }
+}
+
+// Internal: Filter and find all elements matching the selector.
+//
+// Where $.fn.find only matches descendants, findAll will test all the
+// top level elements in the jQuery object as well.
+//
+// elems    - jQuery object of Elements
+// selector - String selector to match
+//
+// Returns a jQuery object.
+function findAll(elems, selector) {
+  return elems.filter(selector).add(elems.find(selector));
+}
+
+function parseHTML(html) {
+  return $.parseHTML(html, document, true)
+}
+
+// Internal: Extracts container and metadata from response.
+//
+// 1. Extracts X-PJAX-URL header if set
+// 2. Extracts inline <title> tags
+// 3. Builds response Element and extracts fragment if set
+//
+// data    - String response data
+// xhr     - XHR response
+// options - pjax options Object
+//
+// Returns an Object with url, title, and contents keys.
+function extractContainer(data, xhr, options) {
+  var obj = {}
+
+  // Prefer X-PJAX-URL header if it was set, otherwise fallback to
+  // using the original requested url.
+  obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
+
+  // Attempt to parse response html into elements
+  if (/<html/i.test(data)) {
+    var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
+    var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
+  } else {
+    var $head = $body = $(parseHTML(data))
+  }
+
+  // If response data is empty, return fast
+  if ($body.length === 0)
+    return obj
+
+  // If there's a <title> tag in the header, use it as
+  // the page's title.
+  obj.title = findAll($head, 'title').last().text()
+
+  if (options.fragment) {
+    // If they specified a fragment, look for it in the response
+    // and pull it out.
+    if (options.fragment === 'body') {
+      var $fragment = $body
+    } else {
+      var $fragment = findAll($body, options.fragment).first()
+    }
+
+    if ($fragment.length) {
+      obj.contents = $fragment.contents()
+
+      // If there's no title, look for data-title and title attributes
+      // on the fragment
+      if (!obj.title)
+        obj.title = $fragment.attr('title') || $fragment.data('title')
+    }
+
+  } else if (!/<html/i.test(data)) {
+    obj.contents = $body
+  }
+
+  // Clean up any <title> tags
+  if (obj.contents) {
+    // Remove any parent title elements
+    obj.contents = obj.contents.not(function() { return $(this).is('title') })
+
+    // Then scrub any titles from their descendants
+    obj.contents.find('title').remove()
+
+    // Gather all script[src] elements
+    obj.scripts = findAll(obj.contents, 'script[src]').remove()
+    obj.contents = obj.contents.not(obj.scripts)
+  }
+
+  // Trim any whitespace off the title
+  if (obj.title) obj.title = $.trim(obj.title)
+
+  return obj
+}
+
+// Load an execute scripts using standard script request.
+//
+// Avoids jQuery's traditional $.getScript which does a XHR request and
+// globalEval.
+//
+// scripts - jQuery object of script Elements
+//
+// Returns nothing.
+function executeScriptTags(scripts) {
+  if (!scripts) return
+
+  var existingScripts = $('script[src]')
+
+  scripts.each(function() {
+    var src = this.src
+    var matchedScripts = existingScripts.filter(function() {
+      return this.src === src
+    })
+    if (matchedScripts.length) return
+
+    var script = document.createElement('script')
+    script.type = $(this).attr('type')
+    script.src = $(this).attr('src')
+    document.head.appendChild(script)
+  })
+}
+
+// Internal: History DOM caching class.
+var cacheMapping      = {}
+var cacheForwardStack = []
+var cacheBackStack    = []
+
+// Push previous state id and container contents into the history
+// cache. Should be called in conjunction with `pushState` to save the
+// previous container contents.
+//
+// id    - State ID Number
+// value - DOM Element to cache
+//
+// Returns nothing.
+function cachePush(id, value) {
+  cacheMapping[id] = value
+  cacheBackStack.push(id)
+
+  // Remove all entires in forward history stack after pushing
+  // a new page.
+  while (cacheForwardStack.length)
+    delete cacheMapping[cacheForwardStack.shift()]
+
+  // Trim back history stack to max cache length.
+  while (cacheBackStack.length > pjax.defaults.maxCacheLength)
+    delete cacheMapping[cacheBackStack.shift()]
+}
+
+// Shifts cache from directional history cache. Should be
+// called on `popstate` with the previous state id and container
+// contents.
+//
+// direction - "forward" or "back" String
+// id        - State ID Number
+// value     - DOM Element to cache
+//
+// Returns nothing.
+function cachePop(direction, id, value) {
+  var pushStack, popStack
+  cacheMapping[id] = value
+
+  if (direction === 'forward') {
+    pushStack = cacheBackStack
+    popStack  = cacheForwardStack
+  } else {
+    pushStack = cacheForwardStack
+    popStack  = cacheBackStack
+  }
+
+  pushStack.push(id)
+  if (id = popStack.pop())
+    delete cacheMapping[id]
+}
+
+// Public: Find version identifier for the initial page load.
+//
+// Returns String version or undefined.
+function findVersion() {
+  return $('meta').filter(function() {
+    var name = $(this).attr('http-equiv')
+    return name && name.toUpperCase() === 'X-PJAX-VERSION'
+  }).attr('content')
+}
+
+// Install pjax functions on $.pjax to enable pushState behavior.
+//
+// Does nothing if already enabled.
+//
+// Examples
+//
+//     $.pjax.enable()
+//
+// Returns nothing.
+function enable() {
+  $.fn.pjax = fnPjax
+  $.pjax = pjax
+  $.pjax.enable = $.noop
+  $.pjax.disable = disable
+  $.pjax.click = handleClick
+  $.pjax.submit = handleSubmit
+  $.pjax.reload = pjaxReload
+  $.pjax.defaults = {
+    timeout: 650,
+    push: true,
+    replace: false,
+    type: 'GET',
+    dataType: 'html',
+    scrollTo: 0,
+    maxCacheLength: 20,
+    version: findVersion
+  }
+  $(window).on('popstate.pjax', onPjaxPopstate)
+}
+
+// Disable pushState behavior.
+//
+// This is the case when a browser doesn't support pushState. It is
+// sometimes useful to disable pushState for debugging on a modern
+// browser.
+//
+// Examples
+//
+//     $.pjax.disable()
+//
+// Returns nothing.
+function disable() {
+  $.fn.pjax = function() { return this }
+  $.pjax = fallbackPjax
+  $.pjax.enable = enable
+  $.pjax.disable = $.noop
+  $.pjax.click = $.noop
+  $.pjax.submit = $.noop
+  $.pjax.reload = function() { window.location.reload() }
+
+  $(window).off('popstate.pjax', onPjaxPopstate)
+}
+
+
+// Add the state property to jQuery's event object so we can use it in
+// $(window).bind('popstate')
+if ( $.inArray('state', $.event.props) < 0 )
+  $.event.props.push('state')
+
+// Is pjax supported by this browser?
+$.support.pjax =
+  window.history && window.history.pushState && window.history.replaceState &&
+  // pushState isn't reliable on iOS until 5.
+  !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
+
+$.support.pjax ? enable() : disable()
+
+})(jQuery);
diff --git a/static/js/select2-3-4-1.min.js b/static/js/select2-3-4-1.min.js
new file mode 100644
--- /dev/null
+++ b/static/js/select2-3-4-1.min.js
@@ -0,0 +1,22 @@
+/*
+Copyright 2012 Igor Vaynberg
+
+Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013
+
+This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
+General Public License version 2 (the "GPL License"). You may choose either license to govern your
+use of this software only upon the condition that you accept all of the terms of either the Apache
+License or the GPL License.
+
+You may obtain a copy of the Apache License and the GPL License at:
+
+http://www.apache.org/licenses/LICENSE-2.0
+http://www.gnu.org/licenses/gpl-2.0.html
+
+Unless required by applicable law or agreed to in writing, software distributed under the Apache License
+or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+either express or implied. See the Apache License and the GPL License for the specific language governing
+permissions and limitations under the Apache License and the GPL License.
+*/
+(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function m(a,b){for(var c=0,d=b.length;d>c;c+=1)if(o(a,b[c]))return c;return-1}function n(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function o(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function p(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function q(a){return a.outerWidth(!1)-a.width()}function r(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function s(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function t(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function u(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function v(a,b){var c=t(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){m(a.target,b.get())>=0&&c(a)})}function w(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function x(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function y(a){a.preventDefault(),a.stopPropagation()}function z(a){a.preventDefault(),a.stopImmediatePropagation()}function A(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function B(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function C(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push("<span class='select2-match'>"),d.push(e(a.substring(f,f+g))),d.push("</span>"),d.push(e(a.substring(f+g,a.length))),b)}function D(a){var b={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function E(c){var d,e=0,f=null,g=c.quietMillis||100,h=c.url,i=this;return function(j){window.clearTimeout(d),d=window.setTimeout(function(){e+=1;var d=e,g=c.data,k=h,l=c.transport||a.fn.select2.ajaxDefaults.transport,m={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},n=a.extend({},a.fn.select2.ajaxDefaults.params,m);g=g?g.call(i,j.term,j.page,j.context):null,k="function"==typeof k?k.call(i,j.term,j.page,j.context):k,f&&f.abort(),c.params&&(a.isFunction(c.params)?a.extend(n,c.params.call(i)):a.extend(n,c.params)),a.extend(n,{url:k,dataType:c.dataType,data:g,success:function(a){if(!(e>d)){var b=c.results(a,j.page);j.callback(b)}}}),f=l.call(i,n)},g)}}function F(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=h.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function G(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function H(b,c){if(a.isFunction(b))return!0;if(!b)return!1;throw Error(c+" must be a function or a falsy value")}function I(b){return a.isFunction(b)?b():b}function J(b){var c=0;return a.each(b,function(a,b){b.children?c+=J(b.children):c++}),c}function K(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(o(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function L(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,j,k,i={x:0,y:0},c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="<div class='select2-measure-scrollbar'></div>";j=a(document),g=function(){var a=1;return function(){return a++}}(),j.on("mousemove",function(a){i.x=a.pageX,i.y=a.pageY}),d=L(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,h,i,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=u(function(){return c.element.closest("body")}),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(I(c.containerCss)),this.container.addClass(I(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(I(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.resultsPage=0,this.context=null,this.initContainer(),s(this.results),this.dropdown.on("mousemove-filtered touchstart touchmove touchend",f,this.bind(this.highlightUnderEvent)),v(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),y(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),y(a))}),r(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||n(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element,c=a.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),c!==b&&(c.container.remove(),c.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show())},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:o(a.attr("locked"),"locked")||o(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:b},prepareOpts:function(c){var d,e,f,g,h=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a <select> element.")}),c=a.extend({},{populateResults:function(d,e,f){var g,l=this.opts.id;g=function(d,e,i){var j,k,m,n,o,p,q,r,s,t;for(d=c.sortResults(d,e,f),j=0,k=d.length;k>j;j+=1)m=d[j],o=m.disabled===!0,n=!o&&l(m)!==b,p=m.children&&m.children.length>0,q=a("<li></li>"),q.addClass("select2-results-dept-"+i),q.addClass("select2-result"),q.addClass(n?"select2-result-selectable":"select2-result-unselectable"),o&&q.addClass("select2-disabled"),p&&q.addClass("select2-result-with-children"),q.addClass(h.opts.formatResultCssClass(m)),r=a(document.createElement("div")),r.addClass("select2-result-label"),t=c.formatResult(m,r,f,h.opts.escapeMarkup),t!==b&&r.html(t),q.append(r),p&&(s=a("<ul></ul>"),s.addClass("select2-result-sub"),g(m.children,s,i+1),q.append(s)),q.data("select2-data",m),e.append(q)},g(e,d,0)}},a.fn.select2.defaults,c),"function"!=typeof c.id&&(f=c.id,c.id=function(a){return a[f]}),a.isArray(c.element.data("select2Tags"))){if("tags"in c)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+c.element.attr("id");c.tags=c.element.data("select2Tags")}if(e?(c.query=this.bind(function(a){var f,g,i,c={results:[],more:!1},e=a.term;i=function(b,c){var d;b.is("option")?a.matcher(e,b.text(),b)&&c.push(h.optionToData(b)):b.is("optgroup")&&(d=h.optionToData(b),b.children().each2(function(a,b){i(b,d.children)}),d.children.length>0&&c.push(d))},f=d.children(),this.getPlaceholder()!==b&&f.length>0&&(g=this.getPlaceholderOption(),g&&(f=f.not(g))),f.each2(function(a,b){i(b,c.results)}),a.callback(c)}),c.id=function(a){return a.id},c.formatResultCssClass=function(a){return a.css}):"query"in c||("ajax"in c?(g=c.element.data("ajax-url"),g&&g.length>0&&(c.ajax.url=g),c.query=E.call(c.element,c.ajax)):"data"in c?c.query=F(c.data):"tags"in c&&(c.query=G(c.tags),c.createSearchChoice===b&&(c.createSearchChoice=function(a){return{id:a,text:a}}),c.initSelection===b&&(c.initSelection=function(d,e){var f=[];a(p(d.val(),c.separator)).each(function(){var d=this,e=this,g=c.tags;a.isFunction(g)&&(g=g()),a(g).each(function(){return o(this.id,d)?(e=this.text,!1):b}),f.push({id:d,text:e})}),e(f)}))),"function"!=typeof c.query)throw"query function not defined for Select2 "+c.element.attr("id");return c},monitorSource:function(){var c,a=this.opts.element;a.on("change.select2",this.bind(function(){this.opts.element.data("select2-change-triggered")!==!0&&this.initSelection()})),c=this.bind(function(){var d,f=a.prop("disabled");f===b&&(f=!1),this.enable(!f);var d=a.prop("readonly");d===b&&(d=!1),this.readonly(d),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.addClass(I(this.opts.containerCssClass)),B(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(I(this.opts.dropdownCssClass))}),a.on("propertychange.select2 DOMAttrModified.select2",c),this.mutationCallback===b&&(this.mutationCallback=function(a){a.forEach(c)}),"undefined"!=typeof WebKitMutationObserver&&(this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),this.propertyObserver=new WebKitMutationObserver(this.mutationCallback),this.propertyObserver.observe(a.get(0),{attributes:!0,subtree:!1}))},triggerSelect:function(b){var c=a.Event("select2-selecting",{val:this.id(b),object:b});return this.opts.element.trigger(c),!c.isDefaultPrevented()},triggerChange:function(b){b=b||{},b=a.extend({},b,{type:"change",val:this.val()}),this.opts.element.data("select2-change-triggered",!0),this.opts.element.trigger(b),this.opts.element.data("select2-change-triggered",!1),this.opts.element.click(),this.opts.blurOnChange&&this.opts.element.blur()},isInterfaceEnabled:function(){return this.enabledInterface===!0},enableInterface:function(){var a=this._enabled&&!this._readonly,b=!a;return a===this.enabledInterface?!1:(this.container.toggleClass("select2-container-disabled",b),this.close(),this.enabledInterface=a,!0)},enable:function(a){return a===b&&(a=!0),this._enabled===a?!1:(this._enabled=a,this.opts.element.prop("disabled",!a),this.enableInterface(),!0)},readonly:function(a){return a===b&&(a=!1),this._readonly===a?!1:(this._readonly=a,this.opts.element.prop("readonly",a),this.enableInterface(),!0)},opened:function(){return this.container.hasClass("select2-dropdown-open")},positionDropdown:function(){var q,r,s,t,b=this.dropdown,c=this.container.offset(),d=this.container.outerHeight(!1),e=this.container.outerWidth(!1),f=b.outerHeight(!1),g=a(window).scrollLeft()+a(window).width(),h=a(window).scrollTop()+a(window).height(),i=c.top+d,j=c.left,l=h>=i+f,m=c.top-f>=this.body().scrollTop(),n=b.outerWidth(!1),o=g>=j+n,p=b.hasClass("select2-drop-above");this.opts.dropdownAutoWidth?(t=a(".select2-results",b)[0],b.addClass("select2-drop-auto-width"),b.css("width",""),n=b.outerWidth(!1)+(t.scrollHeight===t.clientHeight?0:k.width),n>e?e=n:n=e,o=g>=j+n):this.container.removeClass("select2-drop-auto-width"),"static"!==this.body().css("position")&&(q=this.body().offset(),i-=q.top,j-=q.left),p?(r=!0,!m&&l&&(r=!1)):(r=!1,!l&&m&&(r=!0)),o||(j=c.left+e-n),r?(i=c.top-f,this.container.addClass("select2-drop-above"),b.addClass("select2-drop-above")):(this.container.removeClass("select2-drop-above"),b.removeClass("select2-drop-above")),s=a.extend({top:i,left:j,width:e},I(this.opts.dropdownCss)),b.css(s)},shouldOpen:function(){var b;return this.opened()?!1:this._enabled===!1||this._readonly===!0?!1:(b=a.Event("select2-opening"),this.opts.element.trigger(b),!b.isDefaultPrevented())},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")},open:function(){return this.shouldOpen()?(this.opening(),!0):!1},opening:function(){function i(){return{width:Math.max(document.documentElement.scrollWidth,a(window).width()),height:Math.max(document.documentElement.scrollHeight,a(window).height())}}var f,g,b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.addClass("select2-dropdown-open").addClass("select2-container-active"),this.clearDropdownAlignmentPreference(),this.dropdown[0]!==this.body().children().last()[0]&&this.dropdown.detach().appendTo(this.body()),f=a("#select2-drop-mask"),0==f.length&&(f=a(document.createElement("div")),f.attr("id","select2-drop-mask").attr("class","select2-drop-mask"),f.hide(),f.appendTo(this.body()),f.on("mousedown touchstart click",function(b){var d,c=a("#select2-drop");c.length>0&&(d=c.data("select2"),d.opts.selectOnBlur&&d.selectHighlighted({noFocus:!0}),d.close(),b.preventDefault(),b.stopPropagation())})),this.dropdown.prev()[0]!==f[0]&&this.dropdown.before(f),a("#select2-drop").removeAttr("id"),this.dropdown.attr("id","select2-drop"),g=i(),f.css(g).show(),this.dropdown.show(),this.positionDropdown(),this.dropdown.addClass("select2-drop-active");var h=this;this.container.parents().add(window).each(function(){a(this).on(d+" "+c+" "+e,function(){var c=i();a("#select2-drop-mask").css(c),h.positionDropdown()})})},close:function(){if(this.opened()){var b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.parents().add(window).each(function(){a(this).off(c).off(d).off(e)}),this.clearDropdownAlignmentPreference(),a("#select2-drop-mask").hide(),this.dropdown.removeAttr("id"),this.dropdown.hide(),this.container.removeClass("select2-dropdown-open"),this.results.empty(),this.clearSearch(),this.search.removeClass("select2-active"),this.opts.element.trigger(a.Event("select2-close"))}},externalSearch:function(a){this.open(),this.search.val(a),this.updateResults(!1)},clearSearch:function(){},getMaximumSelectionSize:function(){return I(this.opts.maximumSelectionSize)},ensureHighlightVisible:function(){var d,e,f,g,h,i,j,c=this.results;if(e=this.highlight(),!(0>e)){if(0==e)return c.scrollTop(0),b;d=this.findHighlightableChoices().find(".select2-result-label"),f=a(d[e]),g=f.offset().top+f.outerHeight(!0),e===d.length-1&&(j=c.find("li.select2-more-results"),j.length>0&&(g=j.offset().top+j.outerHeight(!0))),h=c.offset().top+c.outerHeight(!0),g>h&&c.scrollTop(c.scrollTop()+(g-h)),i=f.offset().top-c.offset().top,0>i&&"none"!=f.css("display")&&c.scrollTop(c.scrollTop()+i)}},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)")},moveHighlight:function(b){for(var c=this.findHighlightableChoices(),d=this.highlight();d>-1&&c.length>d;){d+=b;var e=a(c[d]);if(e.hasClass("select2-result-selectable")&&!e.hasClass("select2-disabled")&&!e.hasClass("select2-selected")){this.highlight(d);break}}},highlight:function(c){var e,f,d=this.findHighlightableChoices();return 0===arguments.length?m(d.filter(".select2-highlighted")[0],d.get()):(c>=d.length&&(c=d.length-1),0>c&&(c=0),this.results.find(".select2-highlighted").removeClass("select2-highlighted"),e=a(d[c]),e.addClass("select2-highlighted"),this.ensureHighlightVisible(),f=e.data("select2-data"),f&&this.opts.element.trigger({type:"select2-highlight",val:this.id(f),choice:f}),b)},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(b){var c=a(b.target).closest(".select2-result-selectable");if(c.length>0&&!c.is(".select2-highlighted")){var d=this.findHighlightableChoices();this.highlight(d.index(c))}else 0==c.length&&this.results.find(".select2-highlighted").removeClass("select2-highlighted")},loadMoreIfNeeded:function(){var c,a=this.results,b=a.find("li.select2-more-results"),e=this.resultsPage+1,f=this,g=this.search.val(),h=this.context;0!==b.length&&(c=b.offset().top-a.offset().top-a.height(),this.opts.loadMorePadding>=c&&(b.addClass("select2-active"),this.opts.query({element:this.opts.element,term:g,page:e,context:h,matcher:this.opts.matcher,callback:this.bind(function(c){f.opened()&&(f.opts.populateResults.call(this,a,c.results,{term:g,page:e,context:h}),f.postprocessResults(c,!1,!1),c.more===!0?(b.detach().appendTo(a).text(f.opts.formatLoadMore(e+1)),window.setTimeout(function(){f.loadMoreIfNeeded()},10)):b.remove(),f.positionDropdown(),f.resultsPage=e,f.context=c.context)})})))},tokenize:function(){},updateResults:function(c){function l(){d.removeClass("select2-active"),h.positionDropdown()}function m(a){e.html(a),l()}var g,i,d=this.search,e=this.results,f=this.opts,h=this,j=d.val(),k=a.data(this.container,"select2-last-term");if((c===!0||!k||!o(j,k))&&(a.data(this.container,"select2-last-term",j),c===!0||this.showSearchInput!==!1&&this.opened())){var n=this.getMaximumSelectionSize();if(n>=1&&(g=this.data(),a.isArray(g)&&g.length>=n&&H(f.formatSelectionTooBig,"formatSelectionTooBig")))return m("<li class='select2-selection-limit'>"+f.formatSelectionTooBig(n)+"</li>"),b;if(d.val().length<f.minimumInputLength)return H(f.formatInputTooShort,"formatInputTooShort")?m("<li class='select2-no-results'>"+f.formatInputTooShort(d.val(),f.minimumInputLength)+"</li>"):m(""),c&&this.showSearch&&this.showSearch(!0),b;if(f.maximumInputLength&&d.val().length>f.maximumInputLength)return H(f.formatInputTooLong,"formatInputTooLong")?m("<li class='select2-no-results'>"+f.formatInputTooLong(d.val(),f.maximumInputLength)+"</li>"):m(""),b;f.formatSearching&&0===this.findHighlightableChoices().length&&m("<li class='select2-searching'>"+f.formatSearching()+"</li>"),d.addClass("select2-active"),i=this.tokenize(),i!=b&&null!=i&&d.val(i),this.resultsPage=1,f.query({element:f.element,term:d.val(),page:this.resultsPage,context:null,matcher:f.matcher,callback:this.bind(function(g){var i;return this.opened()?(this.context=g.context===b?null:g.context,this.opts.createSearchChoice&&""!==d.val()&&(i=this.opts.createSearchChoice.call(h,d.val(),g.results),i!==b&&null!==i&&h.id(i)!==b&&null!==h.id(i)&&0===a(g.results).filter(function(){return o(h.id(this),h.id(i))}).length&&g.results.unshift(i)),0===g.results.length&&H(f.formatNoMatches,"formatNoMatches")?(m("<li class='select2-no-results'>"+f.formatNoMatches(d.val())+"</li>"),b):(e.empty(),h.opts.populateResults.call(this,e,g.results,{term:d.val(),page:this.resultsPage,context:null}),g.more===!0&&H(f.formatLoadMore,"formatLoadMore")&&(e.append("<li class='select2-more-results'>"+h.opts.escapeMarkup(f.formatLoadMore(this.resultsPage))+"</li>"),window.setTimeout(function(){h.loadMoreIfNeeded()},10)),this.postprocessResults(g,c),l(),this.opts.element.trigger({type:"select2-loaded",items:g}),b)):(this.search.removeClass("select2-active"),b)})})}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0}),this.close(),this.container.removeClass("select2-container-active"),this.search[0]===document.activeElement&&this.search.blur(),this.clearSearch(),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){w(this.search)},selectHighlighted:function(a){var b=this.highlight(),c=this.results.find(".select2-highlighted"),d=c.closest(".select2-result").data("select2-data");d?(this.highlight(b),this.onSelect(d,a)):a&&a.noFocus&&this.close()},getPlaceholder:function(){var a;return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder||((a=this.getPlaceholderOption())!==b?a.text():b)},getPlaceholderOption:function(){if(this.select){var a=this.select.children().first();if(this.opts.placeholderOption!==b)return"first"===this.opts.placeholderOption&&a||"function"==typeof this.opts.placeholderOption&&this.opts.placeholderOption(this.select);if(""===a.text()&&""===a.val())return a}},initContainerWidth:function(){function c(){var c,d,e,f,g;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){if(c=this.opts.element.attr("style"),c!==b)for(d=c.split(";"),f=0,g=d.length;g>f;f+=1)if(e=d[f].replace(/\s/g,"").match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i),null!==e&&e.length>=1)return e[1];return"resolve"===this.opts.width?(c=this.opts.element.css("width"),c.indexOf("%")>0?c:0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px"):null}return a.isFunction(this.opts.width)?this.opts.width():this.opts.width}var d=c.call(this);null!==d&&this.container.css("width",d)}}),e=L(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container"}).html(["<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>","   <span class='select2-chosen'>&nbsp;</span><abbr class='select2-search-choice-close'></abbr>","   <span class='select2-arrow'><b></b></span>","</a>","<input class='select2-focusser select2-offscreen' type='text'/>","<div class='select2-drop select2-display-none'>","   <div class='select2-search'>","       <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>","   </div>","   <ul class='select2-results'>","   </ul>","</div>"].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var b,c,d;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.search.focus(),b=this.search.get(0),b.createTextRange?(c=b.createTextRange(),c.collapse(!1),c.select()):b.setSelectionRange&&(d=this.search.val().length,b.setSelectionRange(d,d)),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown;0>this.opts.minimumResultsForSearch?this.showSearch(!1):this.showSearch(!0),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.focusser.attr("tabindex",this.elementTabIndex),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return y(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.ESC){if(this.opts.openOnEnter===!1&&a.which===c.ENTER)return y(a),b;if(a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),y(a),b}return a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),y(a),b):b}})),r(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),d.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),z(a),this.close(),this.selection.focus())})),d.on("mousedown",this.bind(function(b){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),y(b)})),f.on("mousedown",this.bind(function(){this.search.focus()})),d.on("focus",this.bind(function(a){y(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");if(b){var c=this.getPlaceholderOption();this.opts.element.val(c?c.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection([]),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},isPlaceholderOptionSelected:function(){var a;return(a=this.getPlaceholderOption())!==b&&a.is(":selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find(":selected");b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=o(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,c,d){var e=0,f=this;if(this.findHighlightableChoices().each2(function(a,c){return o(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&(c===!0&&e>=0?this.highlight(e):this.highlight(0)),c===!0){var h=this.opts.minimumResultsForSearch;h>=0&&this.showSearch(J(a.results)>=h)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),o(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c,this.opts.escapeMarkup),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")
+},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),b;if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a,c){var d;return 0===arguments.length?(d=this.selection.data("select2-data"),d==b&&(d=null),d):(a&&""!==a?(d=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),c&&this.triggerChange({added:a,removed:d})):this.clear(c),b)}}),f=L(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["<ul class='select2-choices'>","  <li class='select2-search-field'>","    <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>","  </li>","</ul>","<div class='select2-drop select2-drop-multi select2-display-none'>","   <ul class='select2-results'>","   </ul>","</div>"].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find(":selected").each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=p(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return o(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;e.length>c;c++)for(var g=e[c],h=0;f.length>h;h++){var i=f[h];if(o(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d);var f=this;this.selection.on("mousedown",".select2-search-choice",function(){f.search[0].focus(),f.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var d=e.find(".select2-search-choice-focus"),f=d.prev(".select2-search-choice:not(.select2-locked)"),g=d.next(".select2-search-choice:not(.select2-locked)"),h=x(this.search);if(d.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var i=d;return a.which==c.LEFT&&f.length?i=f:a.which==c.RIGHT?i=g.length?g:null:a.which===c.BACKSPACE?(this.unselect(d.first()),this.search.width(10),i=f.length?f:g):a.which==c.DELETE?(this.unselect(d.first()),this.search.width(10),i=g.length?g:null):a.which==c.ENTER&&(i=null),this.selectChoice(i),y(a),i&&i.length||this.open(),b}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==h.offset&&!h.length)return this.selectChoice(e.find(".select2-search-choice:not(.select2-locked)").last()),y(a),b;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),this.close(),b;case c.ESC:return this.cancel(a),y(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&y(a),a.which===c.ENTER&&y(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("click",d,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",d,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.updateResults(!0),this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>m(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("<li class='select2-search-choice'>    <div></div>    <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a></li>"),f=a("<li class='select2-search-choice select2-locked'><div></div></li>"),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith("<div>"+j+"</div>"),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",y).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),y(b))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=m(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));m(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&H(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("<li class='select2-no-results'>"+g.opts.formatNoMatches(g.search.val())+"</li>")},getMaxSearchWidth:function(){return this.selection.width()-q(this.search)},resizeSearch:function(){var a,b,c,d,e,f=q(this.search);a=A(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),p(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>m(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;b.length>c;c++)for(var d=0;a.length>d;d++)o(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(this.buildChangeDetails(e,this.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c,d){var f,g,e=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(g=this.data(),c||(c=[]),f=a.map(c,function(a){return e.opts.id(a)}),this.setVal(f),this.updateSelection(c),this.clearSearch(),d&&this.triggerChange(this.buildChangeDetails(g,this.data())),b)}}),a.fn.select2=function(){var d,g,h,i,j,c=Array.prototype.slice.call(arguments,0),k=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","readonly","positionDropdown","data","search"],l=["val","opened","isFocused","container","data"],n={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?j=d.element.prop("multiple"):(j=d.multiple||!1,"tags"in d&&(d.multiple=j=!0)),g=j?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>m(c[0],k))throw"Unknown method: "+c[0];if(i=b,g=a(this).data("select2"),g===b)return;if(h=c[0],"container"===h?i=g.container:"dropdown"===h?i=g.dropdown:(n[h]&&(h=n[h]),i=g[h].apply(g,c.slice(1))),m(c[0],l)>=0)return!1}}),i===b?this:i},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return C(a.text,c.term,e,d),e.join("")},formatSelection:function(a,c,d){return a?d(a.text):b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatSelectionCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:K,escapeMarkup:D,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:E,local:F,tags:G},util:{debounce:t,markMatch:C,escapeMarkup:D},"class":{"abstract":d,single:e,multi:f}}}}(jQuery);
diff --git a/templates/abook.julius b/templates/abook.julius
new file mode 100644
--- /dev/null
+++ b/templates/abook.julius
@@ -0,0 +1,44 @@
+$(document).ready(function() {
+    var aType = $("#addressbooktype");
+
+    var searchAbook = function(input, page, callback) {
+        $.ajax({ url: "@{AbookQueryR}"
+               , dataType: 'json'
+               , data: { q : input }
+        })
+        .done(function(data) {
+            addrs = [];
+            if (data) {
+                $(data).each(function() {
+                    addrs.push({id: this, text: this});
+                });
+            }
+            if (callback)
+                callback({results: addrs, more: false});
+            else
+                console.log(addrs);
+        })
+        .fail(function(xhr, st, err) {
+            console.log("Error " + err);
+        });
+    };
+
+    //Set the address book type based on the dropdown.  The google
+    //contacts is set in a seperate change handler which is only
+    //loaded depending on the settings.
+    aType.change(function() {
+        var v = $(this).val();
+        window.localStorage.setItem("Address Book Type", v);
+        if (v == 0)
+            window.searchContacts = undefined;
+        else if (v == 1)
+            window.searchContacts = searchAbook;
+    });
+
+    // Load the old format type
+    var oldformat = window.localStorage.getItem("Address Book Type");
+    if (oldformat) {
+        aType.val(oldformat);
+        aType.change();
+    }
+});
diff --git a/templates/compose.hamlet b/templates/compose.hamlet
--- a/templates/compose.hamlet
+++ b/templates/compose.hamlet
@@ -1,8 +1,18 @@
 $forall e <- err
     <div .alert>
         #{e}
-<form .form-horizontal enctype=#{enctype} method=post action=@{ComposeR}>
+<form enctype=#{enctype} method=post action=@{ComposeR}>
     ^{widget}
     <div .control-group>
         <div .controls>
             <button type="submit" .btn .btn-primary>_{MsgSend}
+            <button type="button" .btn .hidden #previewbtn>_{MsgPreview}
+<form #preview-form enctype=#{previewEnctype} style="display:none" method=post action=@{PreviewMessageR}>
+    ^{previewWidget}
+<div .modal .hide .fade #preview-modal>
+    <div .modal-header>
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;
+        <h3>_{MsgPreview}
+    <div .modal-body #preview-body>
+    <div .modal-footer>
+        <button type="button" .btn data-dismiss="modal" aria-hidden="true">_{MsgClose}
diff --git a/templates/compose.julius b/templates/compose.julius
--- a/templates/compose.julius
+++ b/templates/compose.julius
@@ -1,5 +1,5 @@
 $(document).ready(function() {
-    //copied from jquery-mobile
+    //resize body field: copied from jquery-mobile
     var input = $("textarea#body"),
         extraLineHeight = 15,
         keyupTimeoutBuffer = 100,
@@ -14,8 +14,137 @@
         }
     };
 
+    $(".addr-help").popover();
+
     input.on( "keyup change input paste", function() {
         clearTimeout( keyupTimeout );
         keyupTimeout = setTimeout( input._keyup, keyupTimeoutBuffer );
+    });
+
+    //restore format setting
+    var oldformat = window.localStorage.getItem("Compose Body Format");
+    if (oldformat) {
+        $("#bdyfmt").val(oldformat);
+        if (oldformat == 3) { //markdown
+            $("#previewbtn").removeClass("hidden");
+        }
+    }
+    $("#bdyfmt").change(function() {
+        var f = $("#bdyfmt").val();
+        window.localStorage.setItem("Compose Body Format", f);
+        if (f == 3) { //markdown
+            $("#previewbtn").removeClass("hidden");
+        } else {
+            $("#previewbtn").addClass("hidden");
+        }
+    });
+
+    //The preview button
+    $("#previewbtn").on("click", function() {
+        var frm = $("#preview-form");
+        var data = frm.serializeArray();
+        $(data).each(function() {
+            if (this.name == "f2")
+                this.value = $("#Body").val();
+        });
+        $.ajax({
+            type: "POST",
+            data: $.param(data),
+            dataType: 'html',
+            url: frm.attr('action')
+        })
+        .done(function(data) {
+            $("#preview-body").html(data);
+            $("#preview-modal").modal();
+        })
+        .fail(function(xhr, err, e) {
+            alert("Error " + err + " " + e);
+        });
+        return false;
+    });
+
+    //parse a comma seperated list of addresses, supporting commas inside quotes in the name
+    var parseAddrs = function(str) {
+        var pttrn = /( *"[^"]*"[^,"]*|[^",]+)(,)?/g;
+        var data = [];
+
+        while (m = pttrn.exec(str)) {
+            if (m[1]) {
+                data.push({txt: m[1], comma: m[2] !== undefined});
+            }
+        }
+
+        return data;
+    };
+
+    // Now the address fields
+    $(".address-field").each(function() {
+        $(this).select2({
+            data: [],
+            multiple: true,
+            selectOnBlur: true,
+            width: 'element',
+            minimumInputLength: 3,
+            createSearchChoice: function(term) {
+                return {id: term, text: term};
+            },
+            initSelection: function(element, callback) {
+                var data = []
+                $(parseAddrs(element.val())).each(function() {
+                    data.push({id: this.txt, text: this.txt});
+                });
+                callback(data);
+            },
+            query: function(options) {
+                if (window.searchContacts)
+                    window.searchContacts(options.term, options.page, options.callback);
+                else
+                    options.callback({results:[], more: false});
+            },
+            tokenizer: function(input, selection, selectCallback, opts) {
+                var data = parseAddrs(input);
+                var ret = '';
+                $(data).each(function() {
+                    if (this.comma) {
+                        selectCallback({id: this.txt, text: this.txt});
+                    } else {
+                        ret += this.txt;
+                    }
+                });
+                return ret;
+            },
+            formatSelection: function(addr, node) {
+                if (addr.id != "") {
+                    var esc = this.escapeMarkup;
+                    var m = addr.id.match(/([^<]*)(<([^>]*)>)?/);
+                    if (m === undefined || m[1] === undefined) {
+                        return addr.text;
+                    } else {
+                        node.replaceWith("<div title='"+esc(addr.text)+"'>"+esc(m[1])+"</div>");
+                        return undefined;
+                    }
+                }
+            },
+        });
+
+        var addrbox = $(this);
+        //var s2container = addrbox.prev("div.select2-container");
+        var s2container = addrbox.select2("container");
+        s2container.on("dblclick", "li.select2-search-choice", function() {
+            var data = addrbox.select2("data");
+
+            //Clear the clicked element
+            var item = $(this).children("div").attr("title");
+            var ndata = $.grep(data, function(i) {
+                return i.id != item;
+            });
+            addrbox.select2("data", ndata);
+
+            //Set it as the input
+            s2container.find("li.select2-search-field input").val(item);
+            addrbox.data("select2").resizeSearch();
+            addrbox.data("select2").focusSearch();
+            addrbox.select2("open");
+        });
     });
 });
diff --git a/templates/compose.lucius b/templates/compose.lucius
--- a/templates/compose.lucius
+++ b/templates/compose.lucius
@@ -1,20 +1,20 @@
-textarea#body {
+textarea#Body {
     height: 200px;
 }
-textarea#extraheaders {
+textarea#hdrs {
     white-space: nowrap;
     overflow: auto;
 }
 @media (max-width: 800px) {
-    textarea#body {
+    textarea#Body, input#subj {
         width: 100%;
     }
 }
 @media (min-width: 801px) {
-    textarea#body {
+    textarea#Body, input#subj {
         width: 650px;
     }
-    textarea#extraheaders, div.controls input {
+    textarea#hdrs, div.controls input, div.controls select {
         width: 400px;
     }
 }
diff --git a/templates/default-layout-wrapper.hamlet b/templates/default-layout-wrapper.hamlet
--- a/templates/default-layout-wrapper.hamlet
+++ b/templates/default-layout-wrapper.hamlet
@@ -9,5 +9,12 @@
         <meta name="viewport" content="width=device-width,initial-scale=1">
         <link rel="search" href=@{OpenSearchR} type="application/opensearchdescription+xml" title="Notmuch Web">
         ^{pageHead pc}
+        <noscript>
+            <style>
+                td.only-noscript { display: table-cell; }
+                th.only-noscript { display: inline; }
+                .hide-noscript { display: none; }
+                .collapse { height: auto; }
+                td.search-link { cursor: auto; }
     <body>
         ^{pageBody pc}
diff --git a/templates/default-layout.cassius b/templates/default-layout.cassius
--- a/templates/default-layout.cassius
+++ b/templates/default-layout.cassius
@@ -21,6 +21,8 @@
     body
         padding-right: 5px
         padding-left: 5px
+.only-noscript
+    display: none
 
 /* Fix for https://github.com/twitter/bootstrap/issues/7968 */
 .dropdown-backdrop
diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet
--- a/templates/default-layout.hamlet
+++ b/templates/default-layout.hamlet
@@ -41,8 +41,9 @@
 
   <footer>
     <p>
-      <a href="http://notmuchmail.org/">Notmuch</a> is released under the
-      <a href="http://www.gnu.org/licenses/gpl.html">GPLv3+</a>.
-      Notmuch-web is released under the
-      <a href="http://www.gnu.org/licenses/agpl.html">AGPLv3+</a>(
-      <a href="https://bitbucket.org/wuzzeb/notmuch-web">source</a>).
+      <a href="http://notmuchmail.org/">Notmuch
+      is released under the <a href="http://www.gnu.org/licenses/gpl.html">GPLv3+</a>.
+      <a href="https://bitbucket.org/wuzzeb/notmuch-web">Notmuch-web
+      is released under the
+      <a href="http://www.gnu.org/licenses/agpl.html">AGPLv3+
+      (<a href="#{sourceLink}">source</a>).
diff --git a/templates/google-contacts.hamlet b/templates/google-contacts.hamlet
new file mode 100644
--- /dev/null
+++ b/templates/google-contacts.hamlet
@@ -0,0 +1,8 @@
+<div .control-group>
+    <label .control-label for="addressbooktype">_{MsgAddrBookType}
+    <div .controls>
+        <select name="addressbooktype" #addressbooktype>
+            <option value="0">_{MsgNoAddressBook}
+            <option value="1">_{MsgAbook}
+            <option value="2">_{MsgGoogle}
+        <button #google-auth-button .btn .hidden data-notmuch-client-id="#{clientID}">_{MsgGoogleAuth}
diff --git a/templates/google-contacts.julius b/templates/google-contacts.julius
new file mode 100644
--- /dev/null
+++ b/templates/google-contacts.julius
@@ -0,0 +1,80 @@
+//Some documentation
+//https://developers.google.com/api-client-library/javascript/features/authentication
+//https://developers.google.com/api-client-library/javascript/features/cors
+//https://developers.google.com/google-apps/contacts/v3/
+
+// loadGoogleAuth will be called once the google authentication script has
+// been loaded, and we don't load the script until the document is ready so
+// the body of loadGoogleAuth can assume the document is ready.
+function loadGoogleAuth() {
+    var btn = $("#google-auth-button");
+
+
+    var handleAuth = function(authResult) {
+        if (authResult && !authResult.error) {
+            btn.addClass("hidden");
+        } else {
+            btn.removeClass("hidden");
+        }
+    };
+
+    btn.on('click', function() {
+        gapi.auth.authorize({client_id: btn.data("notmuch-client-id")
+                            , scope: 'https://www.google.com/m8/feeds'
+                            , immediate: false
+                            }, handleAuth);
+
+    });
+
+    // Check auth on document load
+    gapi.auth.authorize({client_id: btn.data("notmuch-client-id")
+                        , scope: 'https://www.google.com/m8/feeds'
+                        , immediate: true
+                        }, handleAuth);
+
+    var searchGoogleContacts = function(input, page, callback) {
+        $.ajax({ url: 'https://www.google.com/m8/feeds/contacts/default/thin'
+               , dataType: 'json'
+               , data: { 'access_token' : gapi.auth.getToken().access_token
+                       , q : input
+                       , 'max-results' : 20
+                       , 'start-index': (page-1)*20 + 1
+                       , 'alt' : "json"
+                       , 'v' : '3.0'
+                       }
+        })
+        .done(function(data) {
+            var num = data.feed["openSearch$totalResults"]["$t"];
+            var addrs = [];
+            $(data.feed.entry).each(function() {
+                var name = this.title["$t"];
+                if ("gd$email" in this) {
+                    $(this["gd$email"]).each(function() {
+                        var a = name + " <" + this.address + ">"
+                        addrs.push({id: a, text: a});
+                    });
+                }
+            });
+            if (callback)
+                callback({results: addrs, more: num >= 20});
+            else
+                console.log(addrs);
+        })
+        .fail(function(xhr, st, err) {
+            alert("Error " + err);
+        });
+    };
+
+    // Set the address book based on the dropdown
+    $("#addressbooktype").change(function() {
+        if ($(this).val() == 2)
+            window.searchContacts = searchGoogleContacts;
+    });
+
+    if ($("#addressbooktype").val() == 2)
+        window.searchContacts = searchGoogleContacts;
+}
+
+$(document).ready(function() {
+    $.getScript("https://apis.google.com/js/auth.js?onload=loadGoogleAuth");
+});
diff --git a/templates/plain-contacts.hamlet b/templates/plain-contacts.hamlet
new file mode 100644
--- /dev/null
+++ b/templates/plain-contacts.hamlet
@@ -0,0 +1,6 @@
+<div .control-group>
+    <label .control-label for="addressbooktype">_{MsgAddrBookType}
+    <div .controls>
+        <select name="addressbooktype" #addressbooktype>
+            <option value="0">_{MsgNoAddressBook}
+            <option value="1">_{MsgAbook}
diff --git a/templates/search.hamlet b/templates/search.hamlet
--- a/templates/search.hamlet
+++ b/templates/search.hamlet
@@ -1,8 +1,9 @@
 <p>
-    <a href=@{ThreadPagerR s}>_{MsgViewInPager}
+    <a .hide-noscript href=@{ThreadPagerR s}>_{MsgViewInPager}
 <table .table .table-striped .table-condensed>
   <thead>
     <tr>
+      <th .only-noscript>
       <th>_{MsgDate}
       <th>_{MsgSubject}
       <th>_{MsgAuthors}
@@ -10,6 +11,8 @@
   <tbody>
     $forall r <- search
       <tr>
+        <td .only-noscript>
+            <a href="@{ThreadR (searchThread r)}">Open
         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchDateRel r}
         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchSubject r}
         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchAuthors r}
diff --git a/tests/main.hs b/tests/main.hs
--- a/tests/main.hs
+++ b/tests/main.hs
@@ -11,10 +11,13 @@
 import Application (makeFoundation)
 
 import HomeTest
+import Address
 
 main :: IO ()
 main = do
     conf <- loadConfig $ (configSettings Testing) { csParseExtra = parseExtra }
     foundation <- makeFoundation conf
-    hspec $ yesodSpec foundation $
-        homeSpecs
+    hspec $ do
+        yesodSpec foundation $
+          homeSpecs
+        addrSpecs
