module Autoproc.Classifier where -- The purpose of this module is to define the abstract and concrete -- syntax for the condition expression language. import Control.Monad.Writer hiding (when) -- Some functions in this module get their meaning and values from -- Configuration module. If you want to change a default such as -- locking, check the Configuration module. import Autoproc.Configuration data EmailAddress = Addr String deriving Show data Mailbox = Mailbox String data CExp = CExp [Flag] Cond Act deriving Show data Flag = Copy | Wait | IgnoreErrors | RawWrite | NeedLock Bool | Chain | CaseSensitive deriving (Eq, Show) data Cond = And Cond Cond | Or Cond Cond | Not Cond | Always | Never | CheckMatch String | CheckHeader String | CheckBody String deriving (Eq, Show) data Act = File String | Fwd [EmailAddress] | Filter String | Nest [CExp] deriving Show --type Rule = Cond -> Act -> m CExp --type Rule = Cond -> Act -> CExp --data RuleM a = RuleM a --------------------------------------------------------------------------- -- Basic functions for manipulating conditions and creating Rules (.&&.) :: Cond -> Cond -> Cond c1 .&&. c2 = And c1 c2 (.||.) :: Cond -> Cond -> Cond c1 .||. c2 = Or c1 c2 subject, body, said :: String -> Cond subject s = CheckHeader ("^Subject.*"++s) body s = CheckBody s said s = subject s .||. body s from, to, to_ :: EmailAddress -> Cond from (Addr s) = CheckHeader ("^From.*"++s) to (Addr s) = CheckHeader ("^TO"++s) to_ (Addr s) = CheckHeader ("^TO_"++s) when :: Cond -> Act -> Writer [CExp] () when c a = whenWithOptions [lock] c a whenWithOptions :: [Flag] -> Cond -> Act -> Writer [CExp] () whenWithOptions fs c a = tell [CExp fs c a] placeIn :: Mailbox -> Act placeIn (Mailbox m) = File m also :: Act -> Act -> Act also (Nest as) (Nest bs) = Nest (flagAllButLast Copy (as++bs)) also (Nest as) b = Nest (flagAllButLast Copy (as++(execWriter $ whenWithOptions [] Always b))) also a (Nest bs) = Nest (flagAllButLast Copy ((execWriter (whenWithOptions [] Always a))++bs)) also a b = Nest (flagAllButLast Copy ((execWriter $ whenWithOptions [] Always a)++ (execWriter $ whenWithOptions [] Always b))) flagAllButLast :: Flag -> [CExp] -> [CExp] flagAllButLast _ [] = [] flagAllButLast f cs = (map (addFlag f) (init cs))++[removeFlag f (last cs)] addFlag :: Flag -> CExp -> CExp addFlag f (CExp fs a c) = (CExp (f:fs) a c) removeFlag :: Flag -> CExp -> CExp removeFlag f (CExp fs a c) = (CExp (filter (/= f) fs) a c) forwardTo :: [EmailAddress] -> Act forwardTo es = Fwd es isSpam :: Cond isSpam = CheckHeader ("^x-spam-status: yes") .||. CheckHeader ("^x-spam-flag: yes") spamLevel :: Int -> Cond spamLevel n = CheckHeader ("^x-spam-Level: "++(concat (replicate n "\\*"))) -------------------------------------------------------------------------- -- Match monad is just the identity monad, this makes it so that the user -- cannot use match arbitrarily. Used a monad instead of just a data -- wrapper because now we can use the monad utilities like liftM data Match a = Match a instance Monad Match where return = Match (>>=) (Match a) f = (f a) match :: Match String match = return "$MATCH" whenMatch :: Match Cond -> Match Act -> Writer [CExp] () whenMatch mc ma = whenMatchWithOptions [lock] mc ma whenMatchWithOptions :: [Flag] -> Match Cond -> Match Act -> Writer [CExp] () whenMatchWithOptions fs (Match c) (Match a) = tell [CExp fs c a] placeInUsingMatch :: Match Mailbox -> Match Act placeInUsingMatch = liftM placeIn (%) :: Cond -> String -> Match Cond (CheckHeader s1) % s2 = return (CheckHeader (s1++"\\/"++s2)) (CheckBody s1) % s2 = return (CheckBody (s1++"\\/"++s2)) (CheckMatch s1) % s2 = return (CheckMatch (s1++"\\/"++s2)) refineBy :: Match Cond -> Match Cond -> Match Cond refineBy = liftM2 (.&&.) alsoUsingMatch :: Match Act -> Match Act -> Match Act alsoUsingMatch = liftM2 also --------------------------------------------------------------------------- -- A few functions to create short hand for sorting sortBy :: (a -> Cond) -> a -> Mailbox -> Writer [CExp] () sortBy f s m = when (f s) (placeIn m) sortByTo_, sortByTo, sortByFrom :: EmailAddress -> Mailbox -> Writer [CExp] () sortByTo_ = sortBy to_ sortByTo = sortBy to sortByFrom = sortBy from sortBySubject :: String -> Mailbox -> Writer [CExp] () sortBySubject = sortBy subject ---------------------------------------------------------------------------- -- Everything below here depends on the values in the Configuration module -- | If the email address (the String argument) contains "foo", then place the email into a folder -- by the name "foo". Actually, the name of the mailbox is created by -- appending boxPrefix which is defined in the Configuration module. simpleSortByFrom :: String -> Writer [CExp] () simpleSortByFrom s = sortByFrom (Addr s) (mailbox s) simpleSortByTo_, simpleSortByTo:: String -> Writer [CExp] () simpleSortByTo s = sortByTo (Addr s) (mailbox s) simpleSortByTo_ s = sortByTo_ (Addr s) (mailbox s) mailbox :: String -> Mailbox mailbox s = Mailbox (boxPrefix++s) mailBoxFromMatch :: Match String -> Match Mailbox mailBoxFromMatch = liftM mailbox lock :: Flag lock = NeedLock lockDefault --------------------------------------------------------------------------- -- This is the actually "Classifier" implementation. It's not as powerful. -- Please consider this "syntax" to be experimental. type Class = (String, [Cond]) type Trigger = (String, Int, Act) type Classifier = Writer [CExp] () mkTrigger :: Trigger -> Classifier mkTrigger (s, i, a) = when (CheckHeader ("^"++(mkHeader s)++(replicate i '*'))) a mkClassifiers :: Class -> Writer [CExp] () mkClassifiers (s, cs) = more (length cs) s cs where more _ _ [] = return () more n t (x:xs) = (when x $ Nest $ incrementHeader t n) >> (more n t xs) incrementHeader :: String -> Int -> [CExp] incrementHeader s n = concat [execWriter (whenMatch ((CheckHeader ("^"++mkHeader s)) % (replicate n '*')) updateHeader), execWriter (when (Not (CheckHeader ("^"++mkHeader s))) writeHeader)] where updateHeader = do { m <- match; return (Filter ("formail -I\""++mkHeader s++m++"*\"")) } writeHeader = Filter ("formail -I\""++mkHeader s++"*\"") mkHeader :: String -> String mkHeader s = "X-classifier-"++s++": " classify :: [Class] -> [Trigger] -> Writer [CExp] () classify cs ts = mapM_ mkClassifiers cs >> mapM_ mkTrigger ts classifyBy :: (String, Cond) -> Act -> Writer [CExp] () classifyBy (s, c) a = classify [(s,[c])] [(s, 1, a)] classifyByAddress::(EmailAddress -> Cond) -> EmailAddress -> Mailbox -> Writer [CExp] () classifyByAddress f e@(Addr s) m = classify [(s, [f e])] [(s, 1, placeIn m)] classifyByTo_, classifyByTo, classifyByFrom:: EmailAddress -> Mailbox -> Writer [CExp] () classifyByTo_ = classifyByAddress to_ classifyByTo = classifyByAddress to classifyByFrom = classifyByAddress from classifyByFromAddr :: String -> String -> Writer [CExp] () classifyByFromAddr x y = classifyByFrom (Addr x) (mailbox y) classifyBySubject :: String -> Mailbox -> Writer [CExp] () classifyBySubject s m = classify [(s, [subject s])] [(s, 1, placeIn m)] simpleClassifyBySubject :: String -> Writer [CExp] () simpleClassifyBySubject x = classifyBySubject x (mailbox x) simpleClassifyByFrom, simpleClassifyByTo_, simpleClassifyByTo::String -> Writer [CExp] () simpleClassifyByFrom s = classifyByFrom (Addr s) (mailbox s) simpleClassifyByTo s = classifyByTo (Addr s) (mailbox s) simpleClassifyByTo_ s = classifyByTo_ (Addr s) (mailbox s) defaultRule :: String -> Writer [CExp] () defaultRule str = when Always $ File str -- | If the subject line contains a certain string, send it to a certain mailbox. subjectToMbox :: String -> String -> Writer [CExp] () subjectToMbox substr mbox = sortBySubject substr $ mailbox mbox -- | As with 'subjectToMbox', except by email address. addressToMbox :: String -> String -> Writer [CExp] () addressToMbox addr mbox = sortByFrom (Addr addr) (mailbox mbox) -- | 'addressToMbox' is fine, but may not work well for mailing lists. toAddressToMbox :: String -> String -> Writer [CExp] () toAddressToMbox addr mbox = sortByTo_ (Addr addr) (mailbox mbox) {- | 'stuffToMbox' is a very general filtering statement, which is intended for specialization by other functions. The idea is to take a logical operator and fold it over a list of strings. If the result is @True@, then the email gets dropped into a specified mailbox. So if you wanted to insist that only an email which has strings @x@, @y@, and @z@ in the subject-line could appear in the xyz mailbox, you'd use .&&. as the logical operator, "xyz" as the @mbox@ argument, [x, y, z] as the list, and a seed value of True. You also need the 'subject' operator, which will map over the list and turn it into properly typed stuff. -} stuffToMbox :: Cond -> (a1 -> a) -> (a -> Cond -> Cond) -> String -> [a1] -> Writer [CExp] () stuffToMbox seed header operator mbox items = when (foldr (operator) seed $ map header items) (insertMbox mbox) -- | If all the strings appear in the subject line, deposit the email in the specified mailbox subjectsToMbox :: [String] -> String -> Writer [CExp] () subjectsToMbox x y = stuffToMbox Always subject (.&&.) y x -- | If any of the strings appear in the subject line, send it to the mbox -- This is currently a bit of a null-op, and I'm not sure it works. anySubjectsToMbox :: [String] -> String -> Writer [CExp] () anySubjectsToMbox x y = stuffToMbox Never subject (.||.) y x -- subjectsNotToMbox = stuffToMbox Never subject ((.||.) .) "" insertMbox :: String -> Act insertMbox = placeIn . mailbox