packages feed

nptools-0.1: label.hs

{-# LANGUAGE OverloadedStrings #-}
import System.Environment
import HSH
import Control.Monad
import Control.Applicative
import Control.Arrow
import Data.Char
-- import Data.Time.Clock
-- import Data.Time.Calendar
import Data.Either
import Data.List
import Data.List.Split (chunk)
import Data.Monoid
import qualified Data.ByteString.Char8 as C
import Data.ByteString (ByteString)
import System.Cmd
import System.IO

(<>) :: Monoid m => m -> m -> m
(<>) = mappend

type Folder   = ByteString
type MsgLabel = ByteString
type MsgID    = ByteString
data Msg = MsgPath     FilePath
         | MsgInFolder Folder Int
         | MsgByID     MsgID
data Labeling a = Label   a
                | Unlabel a
type MsgLabeling = Labeling MsgLabel

parseLabeling :: String -> Either String (Labeling ByteString)
parseLabeling ('+':lbl) = Right . Label   . C.pack $ lbl
parseLabeling ('-':lbl) = Right . Unlabel . C.pack $ lbl
parseLabeling s         = Left s

maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
                [(x, s')] | all isSpace s' -> Just x
                _                          -> Nothing

parseMsg :: String -> Either String Msg
parseMsg ('<':s) = fmap MsgByID . dropEndThis '>' . C.pack $ s
parseMsg ('@':s) = MsgInFolder (C.pack ('+':folder)) <$> maybe err return (maybeRead ix)
  where (folder,ix) = cutOn '/' s
        err         = Left $ "parseMsg: cannot parse message spec: @" ++ s
parseMsg s         = return (MsgPath s)

folderFromMsgLabel :: MsgLabel -> Folder
folderFromMsgLabel = C.cons '+'

cutOnC :: Char -> ByteString -> (ByteString, ByteString)
cutOnC c = second (C.drop 1) . C.break (==c)

cutOn :: Eq a => a -> [a] -> ([a], [a])
cutOn c = second (drop 1) . break (==c)

dropEndThis :: Char -> ByteString -> Either String ByteString
dropEndThis c s | C.last s == c = Right $ C.init s
                | otherwise     = Left $ unwords [ "dropEndThis: unexpected"
                                                 , show (C.last s) ++ ","
                                                 , show c, "was expected" ]

-- dropIt :: Eq a => a -> [a] -> Maybe [a]
-- dropIt x (y:ys) | x == y = Just ys
-- dropIt _ _               = Nothing

pathFromMsgID :: FilePath -> MsgID -> IO (Either String FilePath)
pathFromMsgID message_ids msgID
  = maybe (Left err) (return . C.unpack . snd) . find ((==msgID') . fst) . map (cutOnC ' ') . C.lines
      <$> C.readFile message_ids
      where
        err = C.unpack msgID ++ " not found"
        msgID' = '<' `C.cons` msgID `C.snoc` '>'

pathFromMsg :: FilePath -> Msg -> IO (Either String FilePath)
pathFromMsg _ (MsgPath path)          = return . return $ path
pathFromMsg e (MsgByID  mID)          = pathFromMsgID e mID
pathFromMsg _ (MsgInFolder folder ix) = return <$> runSL ("mhpath" :: String, [C.unpack folder, show ix])

-- mv :: Folder -> Folder -> [Int] -> IO ()
-- mv src dst msgs = run ("refile", msgs ++ [dst, "-nolink", "-src", src])

cp :: [Msg] -> [Folder] -> IO ()
cp msgs dsts | null msgs || null dsts = return ()
cp msgs dsts = do
  message_ids     <- getEnv "NMH_MESSAGE_IDS"
  (errs, pathss)  <- second (chunk 1000) . partitionEithers <$> mapM (pathFromMsg message_ids) msgs
  forM_ pathss $ \paths ->
    -- runIO ("refile", "-link" : (concatMap (("-file":) . pure) paths ++ dsts))
    rawSystem "refile" ("-link" : (concatMap (("-file":) . pure) paths ++ map C.unpack dsts))
  when (not . null $ errs) $ hPutStr stderr $ unlines errs

-- rm :: [Msg] -> [Folder] -> IO ()
-- rm msgs  = do
--   msgsargs <- concat <$> mapM cmdSpecFromMsg msgs
--   run ("rmm", "-unlink" : (msgsargs ++ dsts))

-- archive :: ArchiveFolder -> [Msg] -> IO ()
-- archive archiveFolder = mv "+inbox" archiveFolder

-- unarchive :: ArchiveFolder -> [Msg] -> IO ()
-- unarchive archiveFolder = mv archiveFolder "+inbox"

addLabels :: [MsgLabel] -> [Msg] -> IO ()
addLabels lbls msgs = cp msgs (map folderFromMsgLabel lbls)
  
remLabels :: [MsgLabel] -> [Msg] -> IO ()
remLabels _lbls _msgs = fail "remLabels: not yet implemented"
  
label :: [MsgLabeling] -> [Msg] -> IO ()
label labels msgs = sequence_ [ labelOne lbl msgs | lbl <- labels ]
  where
    labelOne (Label   lbl) = addLabels [lbl]
    labelOne (Unlabel lbl) = remLabels [lbl]
 
applyLabelMap :: [(MsgID, [MsgLabel])] -> IO ()
applyLabelMap lblMap =
  sequence_
  [ addLabels lbls [MsgByID msgID]
  | (msgID, lbls) <- lblMap ]

applyLabelMapForMIDs :: [(MsgID, [MsgLabel])] -> [MsgID] -> IO ()
applyLabelMapForMIDs lmap mids =
  sequence_
  [ maybe (err msgID) (flip addLabels [MsgByID msgID]) lbls
  | msgID <- mids
  , let lbls = lookup msgID lmap
  ]
  where err msgID = C.hPutStrLn stderr $ "not found: " <> msgID

parseEntries :: ByteString -> [(MsgID,[MsgLabel])]
parseEntries = map (second (C.words . C.takeWhile (/=')') . C.dropWhile (=='(') . C.tail)
                  . C.break (==' ')) . C.lines



--   if "+inbox" `elem` labelsToAdd
--     then unarchive archiveFolder msgs
--     else if "-inbox" `elem` labelsToRem
--            then archive archiveFolder msgs
--            else fail "Error: both -inbox and +inbox on the command line"
--   cp archiveFolder (delete "+inbox" labelsToAdd)
--   refile ["-nolink"] (delete "-inbox" labelsToRem)
--   where (labelsToAdd, labelsToRem) = partition isAddLabel labels

--         refile _       []   = return ()
--         refile options lbls =
--           run $ ("refile", msgs ++ ["-src", archiveFolder] ++ options ++ map folderFromMsgLabel lbls)

-- -src is excluded on purpose
-- validOptions :: [Option]
-- validOptions = words "draft link nolink preserve nopreserve unlink nounlink file rmmproc normmproc version help"

main :: IO ()
main = do (options, args)  <- partition ("--" `isPrefixOf`) <$> getArgs
          case (sort options, args) of
            (["--lmap"], []) -> applyLabelMap . parseEntries =<< C.getContents
            (["--mids"], [lmapf]) -> do lmap <- parseEntries <$> C.readFile lmapf
                                        applyLabelMapForMIDs lmap . C.lines =<< C.getContents
            ([], _) ->
              let (msgs, lbls) = partitionEithers $ map parseLabeling args in
              label lbls =<< either fail return (mapM parseMsg msgs)
--           (year, _, _) <- (toGregorian . utctDay) `fmap` getCurrentTime
--           let archiveFolder = "+archive-"++show year
--               (labels, msgs) = partition isLabel args
--           label archiveFolder labels msgs
            (_, _) -> fail "Bad arguments"