Metadata.hs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE TemplateHaskell #-}
  3. module Walrus.Backend.Metadata where
  4. import Data.ByteString (ByteString)
  5. import Data.Time.Clock (UTCTime)
  6. import SMTP.Address
  7. import SMTP.Account
  8. import qualified SMTP.Mime as Mime
  9. import SMTP.Response
  10. import Data.Textual.Class
  11. import Data.Time.ISO8601
  12. import Data.IP
  13. import Data.Default.Class
  14. import Control.Applicative
  15. import Control.Lens
  16. import Text.Read (readMaybe)
  17. import Data.Attoparsec.ByteString.Char8 (Parser)
  18. import qualified Data.Attoparsec.ByteString.Char8 as A
  19. import Data.Attoparsec.ByteString.Char8.Extras
  20. import qualified Data.ByteString as BS
  21. import qualified Data.ByteString.Char8 as C8
  22. import qualified Data.List as List
  23. data BackendAction = DATA deriving (Show, Read, Eq, Ord, Bounded, Enum)
  24. data ClientIdentity = ClientIdentity {_clientIp :: IP, _clientPort :: Int} deriving (Show, Read, Ord, Eq)
  25. data MaybeMetadata = MaybeMetadata {_mclientId :: Maybe ClientIdentity, _mclientName :: Maybe ByteString,
  26. _mmailFrom :: Maybe Account, _mrcptTo :: [Address], _mrcptFailed :: [(Address, Response)],
  27. _mauth :: Maybe ByteString, _mrecvDate :: Maybe UTCTime, _mbodyEnc :: Mime.BodyEncoding,
  28. _msmtpUtf8 :: Bool, _maction :: Maybe BackendAction, _munrecognized :: [ByteString],
  29. _mdataSize :: Maybe Int
  30. } deriving (Show, Ord, Eq)
  31. data Metadata = Metadata {_clientId :: ClientIdentity, _clientName :: ByteString,
  32. _mailFrom :: Account, _rcptTo :: [Address], _rcptFailed :: [(Address, Response)],
  33. _auth :: Maybe ByteString, _recvDate :: UTCTime, _bodyEnc :: Mime.BodyEncoding,
  34. _smtpUtf8 :: Bool, _action :: BackendAction, _unrecognized :: [ByteString],
  35. _dataSize :: Int
  36. } deriving (Show, Ord, Eq)
  37. makeLenses ''ClientIdentity
  38. makeLenses ''MaybeMetadata
  39. makeLenses ''Metadata
  40. instance Default MaybeMetadata where
  41. def = MaybeMetadata Nothing Nothing Nothing [] [] Nothing Nothing Mime.B7BitEncoding False Nothing [] Nothing
  42. metadataForClient :: IP -> Int -> MaybeMetadata
  43. metadataForClient c p = def & mclientId .~ (Just (ClientIdentity c p))
  44. resetMetadata :: MaybeMetadata -> MaybeMetadata
  45. resetMetadata d = def & mclientId .~ d^.mclientId & mclientName .~ d^.mclientName
  46. strictMetadata :: MaybeMetadata -> Maybe Metadata
  47. strictMetadata m = do
  48. act <- m^.maction
  49. cid <- m^.mclientId
  50. cnm <- m^.mclientName
  51. rfm <- m^.mmailFrom
  52. let rto = m^.mrcptTo
  53. rfail = m^.mrcptFailed
  54. usr = m^.mauth
  55. rcv <- m^.mrecvDate
  56. let enc = m^.mbodyEnc
  57. utf = m^.msmtpUtf8
  58. unrq = m^.munrecognized
  59. sz <- m^.mdataSize
  60. return $ Metadata cid cnm rfm rto rfail usr rcv enc utf act unrq sz
  61. renderMetadata :: Metadata -> ByteString
  62. renderMetadata m = BS.concat serialize
  63. where
  64. serialize :: [ByteString]
  65. serialize = let
  66. act = m^.action
  67. cid = m^.clientId
  68. cnm = m^.clientName
  69. rfm = m^.mailFrom
  70. rto = m^.rcptTo
  71. rfail = m^.rcptFailed
  72. usr = m^.auth
  73. rcv = m^.recvDate
  74. enc = m^.bodyEnc
  75. utf = m^.smtpUtf8
  76. sz = m^.dataSize
  77. usrStr = case usr of
  78. Nothing -> []
  79. Just u -> ["Auth-User: ", u, "\r\n"]
  80. toStr = List.concat $ map (\x -> ["To: ", renderMetadataAddress $ x, "\r\n"]) rto
  81. failStr = List.concat $ map (\(a, r) -> ["Failed: ", renderMetadataAddress a, renderLineResponse r, "\r\n"]) rfail
  82. unrec = m^.unrecognized
  83. h = [
  84. "Action: ", show act, "\r\n",
  85. "Client-Ip: ", show $ cid^.clientIp, "\r\n",
  86. "Client-Port: ", show $ cid^.clientPort, "\r\n",
  87. "Client-Name: ", fromTextual cnm, "\r\n",
  88. "Return-Path: ", fromTextual . normalAccountName $ rfm, "\r\n",
  89. "Recv-Date: ", formatISO8601 $ rcv, "\r\n",
  90. "Body-Encoding: ", show enc, "\r\n",
  91. "SMTP-UTF8: ", if utf then "Yes" else "No", "\r\n",
  92. "Data-Size: ", show sz, "\r\n"
  93. ] :: [String]
  94. in map fromTextual h ++ toStr ++ failStr ++ usrStr ++ unrec
  95. parseMetadata :: A.Parser Metadata
  96. parseMetadata = do
  97. (m', h', p') <- parserFold parseField (def, Nothing, Nothing)
  98. A.endOfLine
  99. let i = do
  100. h <- h'
  101. p <- p'
  102. return $ ClientIdentity h p
  103. m = set mclientId i m'
  104. case strictMetadata m of
  105. Just s -> return s
  106. Nothing -> failParser
  107. where
  108. parseField :: Parser ((MaybeMetadata, Maybe IP, Maybe Int) -> (MaybeMetadata, Maybe IP, Maybe Int))
  109. parseField = do
  110. A.choice [
  111. do
  112. act <- hdr "Action" parseEnumCI
  113. return $ \(m, ip, p) -> (set maction (Just act) m, ip, p),
  114. do
  115. ip <- hdr "Client-Ip" parseRead
  116. return $ \(m, _, p) -> (m, Just ip, p),
  117. do
  118. p <- hdr "Client-Port" parseRead
  119. return $ \(m, ip, _) -> (m, ip, Just p),
  120. do
  121. nm <- hdr "Client-Name" (A.takeTill A.isSpace)
  122. return $ \(m, ip, p) -> (set mclientName (Just nm) m, ip, p),
  123. do
  124. frm <- hdr "Return-Path" parseAccount
  125. return $ \(m, ip, p) -> (set mmailFrom (Just frm) m, ip, p),
  126. do
  127. rtp <- hdr "To" parseAddress
  128. return $ \(m, ip, p) -> let
  129. crtp = m^.mrcptTo
  130. in (set mrcptTo (rtp:crtp) m, ip, p),
  131. do
  132. rfl <- hdr "Failed" parseAddressingReason
  133. return $ \(m, ip, p) -> let
  134. fld = m^.mrcptFailed
  135. in (set mrcptFailed (rfl:fld) m, ip, p),
  136. do
  137. recv <- hdr "Recv-Date" parseISO8601Val
  138. return $ \(m, ip, p) -> (set mrecvDate (Just recv) m, ip, p),
  139. do
  140. enc <- hdr "Body-Encoding" Mime.parseBodyEncoding
  141. return $ \(m, ip, p) -> (set mbodyEnc enc m, ip, p),
  142. do
  143. utf <- hdr "SMTP-UTF8" parseMetadataBool
  144. return $ \(m, ip, p) -> (set msmtpUtf8 utf m, ip, p),
  145. do
  146. usr <- hdr "Auth-User" bsval
  147. return $ \(m, ip, p) -> (set mauth (Just usr) m, ip, p),
  148. do
  149. sz <- hdr "Data-Size" A.decimal
  150. return $ \(m, ip, p) -> (set mdataSize (Just sz) m, ip, p),
  151. do
  152. u <- entireHdr
  153. return $ \(m, ip, p) -> let
  154. uu = m^.munrecognized
  155. in (set munrecognized (u:uu) m, ip, p)
  156. ]
  157. entireHdr :: Parser ByteString
  158. entireHdr = do
  159. a <- A.satisfy (not . A.isEndOfLine . asW8)
  160. t <- A.takeTill (A.isEndOfLine . asW8)
  161. A.endOfLine
  162. l <- takeLines
  163. return $ BS.concat [C8.cons a t, "\r\n", l]
  164. takeLines :: Parser ByteString
  165. takeLines = do
  166. c' <- A.peekChar
  167. case c' of
  168. Nothing -> return ""
  169. Just c -> if isCHorizontalSpace c
  170. then do
  171. l <- A.takeTill (A.isEndOfLine . asW8)
  172. A.endOfLine
  173. ll <- takeLines
  174. return $ BS.concat [l, "\r\n", ll]
  175. else return ""
  176. hdr :: ByteString -> Parser a -> Parser a
  177. hdr pt f = do
  178. skipHorizontalSpace
  179. A.stringCI pt
  180. skipHorizontalSpace
  181. A.char ':'
  182. skipHorizontalSpace
  183. t <- bsval
  184. r <- case A.parseOnly f t of
  185. Left _ -> failParser
  186. Right v -> return v
  187. skipHorizontalSpace
  188. return r
  189. bsval :: Parser ByteString
  190. bsval = do
  191. ll <- entireHdr
  192. let (b, _) = BS.spanEnd (\x -> x == asW8 '\r' || x == asW8 '\n') ll
  193. return b
  194. parseRead :: Read a => Parser a
  195. parseRead = do
  196. v <- A.takeTill A.isSpace
  197. case readMaybe . fromTextual $ v of
  198. Nothing -> failParser
  199. Just i -> return i
  200. parseISO8601Val = do
  201. v <- A.takeTill A.isSpace
  202. case parseISO8601 . fromTextual $ v of
  203. Nothing -> failParser
  204. Just t -> return t
  205. parseMetadataBool :: Parser Bool
  206. parseMetadataBool = do
  207. A.choice [
  208. A.stringCI "YES" *> return True,
  209. A.stringCI "NO" *> return False
  210. ]
  211. parseAddressingReason :: Parser (Address, Response)
  212. parseAddressingReason = do
  213. a <- parseMetadataAddress
  214. skipHorizontalSpace
  215. A.char ';'
  216. skipHorizontalSpace
  217. r <- parseLineResponse
  218. return (a, r)