diff options
-rw-r--r-- | MBox.hs | 154 | ||||
-rw-r--r-- | test10.hs | 151 |
2 files changed, 161 insertions, 144 deletions
@@ -0,0 +1,154 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module MBox + ( + -- TODO don't re-export MBox but use our own Message type + module Export + , getMessageId + , toForest + ) where + +import qualified Data.MBox as Export + +import Control.Applicative +import qualified Data.CaseInsensitive as CI +import qualified Data.List as List +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Maybe +import Data.MBox +import Data.Ord +import Data.Set (Set) +import qualified Data.Set as Set +import Data.Text.Lazy (Text) +import Data.Time +import Data.Tree (Tree, Forest) +import qualified Data.Tree as Tree +import qualified MappedSets +import qualified Data.Text.Lazy as Text +import Safe +import System.Locale +import qualified Text.ParserCombinators.Parsec.Rfc2822 as P +import qualified Text.ParserCombinators.Parsec as P + + +type Ident = Text + + +data IdentFields = IdentFields + { messageId :: Ident + , inReplyTo :: [Ident] + , references :: [Ident] + } + deriving Show + + +toForest :: MBox -> Forest Message +toForest mbox = + map (sortTree . fmap (\i -> fromMaybe (error "meh") $ Map.lookup i msgs)) $ + concatMap (Tree.subForest . mkSubTree) (Set.toList $ roots refs) + where + + mkSubTree rootLabel = + Tree.Node rootLabel $ + map mkSubTree (maybe [] Set.toList $ Map.lookup rootLabel backRefs) + + refs = mboxRefs mbox + backRefs = MappedSets.invert refs + msgs = unpackMBox mbox + + +roots :: Ord a => Map a (Set a) -> Set a +roots refs = + Set.unions $ Map.elems $ Map.filter p refs + where + messageIDs = Set.fromList $ Map.keys refs + p = Set.null . Set.intersection messageIDs + + +sortTree :: Tree Message -> Tree Message +sortTree t = + Tree.Node (Tree.rootLabel t) $ + map sortTree $ + List.sortBy (comparing $ getMessageDate . Tree.rootLabel) $ + Tree.subForest t + + +getMessageDate :: Message -> Maybe UTCTime +getMessageDate msg = + parseTime defaultTimeLocale rfc822DateFormat =<< + Text.unpack . snd <$> + (lastMay $ + filter ((==CI.mk "Date") . CI.mk . Text.unpack . fst) $ + headers msg) + + +unpackMBox :: MBox -> Map Ident Message +unpackMBox = + Map.fromList . + map (\msg -> (getMessageId $ headers msg, msg)) + + +getIdentFields :: Message -> IdentFields +getIdentFields m = + IdentFields + { messageId = getMessageId hdrs + , inReplyTo = getInReplyTo hdrs + , references = getReferences hdrs + } + where + hdrs = headers m + + +-- TODO generate default Message-ID if not present +getMessageId :: [Header] -> Ident +getMessageId = + head . + headerMessageIds "Message-ID" + + +getInReplyTo :: [Header] -> [Ident] +getInReplyTo = + headerMessageIds "In-Reply-To" + + +getReferences :: [Header] -> [Ident] +getReferences = + headerMessageIds "References" + + +headerMessageIds :: P.SourceName -> [Header] -> [Ident] +headerMessageIds headerName = + concatMap ( + either ((:[]) . Text.pack . show) id . + parseMsgIds headerName . + snd + ) . + filter ((==CI.mk headerName) . CI.mk . Text.unpack . fst) + + +parseMsgIds :: P.SourceName -> Text -> Either P.ParseError [Ident] +parseMsgIds srcName = + either Left (Right . map (Text.init . Text.tail . Text.pack)) . + P.parse obs_in_reply_to_parser srcName . + Text.unpack + where + --obs_in_reply_to_parser :: CharParser a [String] + obs_in_reply_to_parser = + --filter (not . null) <$> P.many (P.phrase >> return [] <|> P.msg_id) + P.many1 P.msg_id + + +messageRefs :: IdentFields -> [Ident] +messageRefs IdentFields{..} = + if null inReplyTo + then maybe [""] (:[]) (lastMay references) + else inReplyTo + + +mboxRefs :: MBox -> Map Ident (Set Ident) +mboxRefs = + MappedSets.mk . + map (\m -> + let x = getIdentFields m + in (messageId x, messageRefs x)) @@ -5,42 +5,17 @@ --module Main (main) where import Control.Applicative ---import qualified Data.Attoparsec.ByteString.Char8 as A8 -import qualified Data.CaseInsensitive as CI -import qualified Data.List as List -import Data.Maybe -import Data.Map.Strict (Map) -import qualified Data.Map.Strict as Map -import Data.MBox -import Data.Ord -import Data.Set (Set) -import qualified Data.Set as Set import qualified Data.Text.Lazy as LT -import Data.Time -import Data.Tree (Tree, Forest) import qualified Data.Tree as Tree -import qualified MappedSets -import Safe +import MBox (MBox) +import qualified MBox import System.Environment -import System.Locale import System.Process -import qualified Text.ParserCombinators.Parsec.Rfc2822 as P -import qualified Text.ParserCombinators.Parsec as P - - -type Ident = LT.Text - -data IdentFields = IdentFields - { messageId :: Ident - , inReplyTo :: [Ident] - , references :: [Ident] - } - deriving Show notmuchShowMBox :: String -> IO MBox notmuchShowMBox searchTerm = - parseMBox . LT.pack <$> readProcess + MBox.parseMBox . LT.pack <$> readProcess "notmuch" [ "show" , "--format=mbox" @@ -50,123 +25,11 @@ notmuchShowMBox searchTerm = "" -getMessageDate :: Message -> Maybe UTCTime -getMessageDate msg = - parseTime defaultTimeLocale rfc822DateFormat =<< - LT.unpack . snd <$> - (lastMay $ - filter ((==CI.mk "Date") . CI.mk . LT.unpack . fst) $ - headers msg) - - -getIdentFields :: Message -> IdentFields -getIdentFields m = - IdentFields - { messageId = getMessageId hdrs - , inReplyTo = getInReplyTo hdrs - , references = getReferences hdrs - } - where - hdrs = headers m - - --- TODO generate default Message-ID if not present -getMessageId :: [Header] -> Ident -getMessageId = - head . - headerMessageIds "Message-ID" - - -getInReplyTo :: [Header] -> [Ident] -getInReplyTo = - headerMessageIds "In-Reply-To" - - -getReferences :: [Header] -> [Ident] -getReferences = - headerMessageIds "References" - - -headerMessageIds :: P.SourceName -> [Header] -> [Ident] -headerMessageIds headerName = - concatMap ( - either ((:[]) . LT.pack . show) id . - parseMsgIds headerName . - snd - ) . - filter ((==CI.mk headerName) . CI.mk . LT.unpack . fst) - - -parseMsgIds :: P.SourceName -> LT.Text -> Either P.ParseError [LT.Text] -parseMsgIds srcName = - either Left (Right . map (LT.init . LT.tail . LT.pack)) . - P.parse obs_in_reply_to_parser srcName . - LT.unpack - where - --obs_in_reply_to_parser :: CharParser a [String] - obs_in_reply_to_parser = - --filter (not . null) <$> P.many (P.phrase >> return [] <|> P.msg_id) - P.many1 P.msg_id - - -messageRefs :: IdentFields -> [Ident] -messageRefs IdentFields{..} = - if null inReplyTo - then maybe [""] (:[]) (lastMay references) - else inReplyTo - - -mboxRefs :: MBox -> Map Ident (Set Ident) -mboxRefs = - MappedSets.mk . - map (\m -> - let x = getIdentFields m - in (messageId x, messageRefs x)) - - -roots :: Ord a => Map a (Set a) -> Set a -roots refs = - Set.unions $ Map.elems $ Map.filter p refs - where - messageIDs = Set.fromList $ Map.keys refs - p = Set.null . Set.intersection messageIDs - - -sortTree :: Tree Message -> Tree Message -sortTree t = - Tree.Node (Tree.rootLabel t) $ - map sortTree $ - List.sortBy (comparing $ getMessageDate . Tree.rootLabel) $ - Tree.subForest t - - -toForest :: MBox -> Forest Message -toForest mbox = - map (sortTree . fmap (\i -> fromMaybe (error "meh") $ Map.lookup i msgs)) $ - concatMap (Tree.subForest . mkSubTree) (Set.toList $ roots refs) - where - - mkSubTree rootLabel = - Tree.Node rootLabel $ - map mkSubTree (maybe [] Set.toList $ Map.lookup rootLabel backRefs) - - refs = mboxRefs mbox - backRefs = MappedSets.invert refs - msgs = unpackMBox mbox - - - -unpackMBox :: MBox -> Map Ident Message -unpackMBox = - Map.fromList . - map (\msg -> (getMessageId $ headers msg, msg)) - - -renderMessage :: Message -> String +renderMessage :: MBox.Message -> String renderMessage msg = - LT.unpack (getMessageId $ headers msg) + LT.unpack (MBox.getMessageId $ MBox.headers msg) ++ " " - ++ drop (length ("From " :: String)) (LT.unpack $ fromLine msg) + ++ drop (length ("From " :: String)) (LT.unpack $ MBox.fromLine msg) main :: IO () @@ -178,4 +41,4 @@ main = do putStrLn . Tree.drawTree . Tree.Node "subject:tree-test" . map (fmap renderMessage) . - toForest + MBox.toForest |