{-# LANGUAGE FlexibleContexts #-}
module Text.Parsec.Rfc2821 where
import Text.Parsec.Rfc2234
import Control.Exception ( assert )
import Control.Monad.State
import Data.Char ( toLower )
import Data.List ( intercalate )
import Text.Parsec hiding (crlf)
{-# ANN module "HLint: ignore Use camelCase" #-}
data EsmtpCmd
= Helo String
| Ehlo String
| MailFrom Mailbox
| RcptTo Mailbox
| Data
| Rset
| Send Mailbox
| Soml Mailbox
| Saml Mailbox
| Vrfy String
| Expn String
| Help String
| Noop
| Quit
| Turn
| WrongArg String ParseError
instance Show EsmtpCmd where
show (Helo str) = "HELO " ++ str
show (Ehlo str) = "EHLO " ++ str
show (MailFrom mbox) = "MAIL FROM:" ++ show mbox
show (RcptTo mbox) = "RCPT TO:" ++ show mbox
show Data = "DATA"
show Rset = "RSET"
show (Send mbox) = "SEND " ++ show mbox
show (Soml mbox) = "SOML " ++ show mbox
show (Saml mbox) = "SAML " ++ show mbox
show (Vrfy str) = "VRFY " ++ str
show (Expn str) = "EXPN " ++ str
show Noop = "NOOP"
show Quit = "QUIT"
show Turn = "TURN"
show (Help t)
| null t = "HELP"
| otherwise = "HELP " ++ t
show (WrongArg str _) = "Syntax error in argument of " ++ str ++ "."
data Mailbox = Mailbox [String] String String
instance Eq Mailbox where
lhs == rhs = norm lhs == norm rhs
where
norm (Mailbox rt lp hp) = (rt, lp, map toLower hp)
instance Show Mailbox where
show (Mailbox [] [] []) = "<>"
show (Mailbox [] "postmaster" []) = "<postmaster>"
show (Mailbox p u d) = "<" ++ route ++ (if null route then [] else ":") ++ mbox ++ ">"
where
route = intercalate "," . map ((:) '@') $ p
mbox = u ++ "@" ++ d
instance Read Mailbox where
readsPrec _ = parsec2read (path <|> mailbox)
readList = error "reading [Mailbox] is not supported"
nullPath :: Mailbox
nullPath = Mailbox [] [] []
postmaster :: Mailbox
postmaster = Mailbox [] "postmaster" []
data EsmtpReply = Reply EsmtpCode [String]
data EsmtpCode = Code SuccessCode Category Int
data SuccessCode
= Unused0
| PreliminarySuccess
| Success
| IntermediateSuccess
| TransientFailure
| PermanentFailure
deriving (Enum, Bounded, Eq, Ord, Show)
data Category
= Syntax
| Information
| Connection
| Unspecified3
| Unspecified4
| MailSystem
deriving (Enum, Bounded, Eq, Ord, Show)
instance Show EsmtpReply where
show (Reply c@(Code suc cat _) []) =
let msg = show suc ++ " in category " ++ show cat
in
show $ Reply c [msg]
show (Reply code msg) =
let prefixCon = show code ++ "-"
prefixEnd = show code ++ " "
fmt p l = p ++ l ++ "\r\n"
(x:xs) = reverse msg
msgCon = map (fmt prefixCon) xs
msgEnd = fmt prefixEnd x
msg' = reverse (msgEnd:msgCon)
in
concat msg'
instance Show EsmtpCode where
show (Code suc cat n) =
assert (n >= 0 && n <= 9) $
(show . fromEnum) suc ++ (show . fromEnum) cat ++ show n
reply :: Int -> Int -> Int -> [String] -> EsmtpReply
reply suc c n msg =
assert (suc >= 0 && suc <= 5) $
assert (c >= 0 && c <= 5) $
assert (n >= 0 && n <= 9) $
Reply (Code (toEnum suc) (toEnum c) n) msg
isSuccess :: EsmtpReply -> Bool
isSuccess (Reply (Code PreliminarySuccess _ _) _) = True
isSuccess (Reply (Code Success _ _) _) = True
isSuccess (Reply (Code IntermediateSuccess _ _) _) = True
isSuccess _ = False
isFailure :: EsmtpReply -> Bool
isFailure (Reply (Code PermanentFailure _ _) _) = True
isFailure (Reply (Code TransientFailure _ _) _) = True
isFailure _ = False
isShutdown :: EsmtpReply -> Bool
isShutdown (Reply (Code Success Connection 1) _) = True
isShutdown (Reply (Code TransientFailure Connection 1) _) = True
isShutdown _ = False
smtpCmd :: Stream s m Char => ParsecT s u m EsmtpCmd
smtpCmd = choice
[ smtpData, rset, noop, quit, turn
, helo, mail, rcpt, send, soml, saml
, vrfy, expn, help, ehlo
]
smtpData :: Stream s m Char => ParsecT s u m EsmtpCmd
rset, quit, turn, helo, ehlo, mail :: Stream s m Char => ParsecT s u m EsmtpCmd
rcpt, send, soml, saml, vrfy, expn :: Stream s m Char => ParsecT s u m EsmtpCmd
help :: Stream s m Char => ParsecT s u m EsmtpCmd
noop :: Stream s m Char => ParsecT s u m EsmtpCmd
smtpData = mkCmd0 "DATA" Data
rset = mkCmd0 "RSET" Rset
quit = mkCmd0 "QUIT" Quit
turn = mkCmd0 "TURN" Turn
helo = mkCmd1 "HELO" Helo domain
ehlo = mkCmd1 "EHLO" Ehlo domain
mail = mkCmd1 "MAIL" MailFrom from_path
rcpt = mkCmd1 "RCPT" RcptTo to_path
send = mkCmd1 "SEND" Send from_path
soml = mkCmd1 "SOML" Soml from_path
saml = mkCmd1 "SAML" Saml from_path
vrfy = mkCmd1 "VRFY" Vrfy word
expn = mkCmd1 "EXPN" Expn word
help = try (mkCmd0 "HELP" (Help [])) <|>
mkCmd1 "HELP" Help (option [] word)
noop = try (mkCmd0 "NOOP" Noop) <|>
mkCmd1 "NOOP" (const Noop) (option [] word)
from_path :: Stream s m Char => ParsecT s u m Mailbox
from_path = do
caseString "from:"
(try (string "<>" >> return nullPath) <|> path)
<?> "from-path"
to_path :: Stream s m Char => ParsecT s u m Mailbox
to_path = do
caseString "to:"
(try (caseString "<postmaster>" >> return postmaster)
<|> path) <?> "to-path"
path :: Stream s m Char => ParsecT s u m Mailbox
path = between (char '<') (char '>') (p <?> "path")
where
p = do
r1 <- option [] (a_d_l >>= \r -> char ':' >> return r)
(Mailbox _ l d) <- mailbox
return (Mailbox r1 l d)
mailbox :: Stream s m Char => ParsecT s u m Mailbox
mailbox = (Mailbox [] <$> local_part <* char '@' <*> domain) <?> "mailbox"
local_part :: Stream s m Char => ParsecT s u m String
local_part = (dot_string <|> quoted_string) <?> "local-part"
domain :: Stream s m Char => ParsecT s u m String
domain = choice
[ tokenList subdomain '.' <?> "domain"
, address_literal <?> "address literal"
]
a_d_l :: Stream s m Char => ParsecT s u m [String]
a_d_l = sepBy1 at_domain (char ',') <?> "route-list"
at_domain :: Stream s m Char => ParsecT s u m String
at_domain = (char '@' >> domain) <?> "at-domain"
address_literal :: Stream s m Char => ParsecT s u m String
address_literal = ipv4_literal <?> "IPv4 address literal"
ipv4_literal :: Stream s m Char => ParsecT s u m String
ipv4_literal = do
rs <- between (char '[') (char ']') ipv4addr
return ('[': reverse (']': reverse rs))
ipv4addr :: Stream s m Char => ParsecT s u m String
ipv4addr = p <?> "IPv4 address literal"
where
p = do
r1 <- snum
r2 <- char '.' >> snum
r3 <- char '.' >> snum
r4 <- char '.' >> snum
return (r1 ++ "." ++ r2 ++ "." ++ r3 ++ "." ++ r4)
subdomain :: Stream s m Char => ParsecT s u m String
subdomain = p <?> "domain name"
where
p = do
r <- many1 (alpha <|> digit <|> char '-')
if last r == '-'
then fail "subdomain must not end with hyphen"
else return r
dot_string :: Stream s m Char => ParsecT s u m String
dot_string = tokenList atom '.' <?> "dot_string"
atom :: Stream s m Char => ParsecT s u m String
atom = many1 atext <?> "atom"
where
atext = alpha <|> digit <|> oneOf "!#$%&'*+-/=?^_`{|}~"
snum :: Stream s m Char => ParsecT s u m String
snum = do
r <- manyNtoM 1 3 digit
if (read r :: Int) > 255
then fail "IP address parts must be 0 <= x <= 255"
else return r
number :: Stream s m Char => ParsecT s u m String
number = many1 digit
word :: Stream s m Char => ParsecT s u m String
word = (atom <|> fmap show quoted_string)
<?> "word or quoted-string"
{-# ANN fixCRLF "HLint: ignore Use list literal pattern" #-}
fixCRLF :: String -> String
fixCRLF ('\r' :'\n':[]) = fixCRLF []
fixCRLF ( x :'\n':[]) = x : fixCRLF []
fixCRLF ( x : xs ) = x : fixCRLF xs
fixCRLF [ ] = "\r\n"
mkCmd0 :: Stream s m Char => String -> a -> ParsecT s u m a
mkCmd0 str cons = (do
try (caseString str)
_ <- skipMany wsp >> crlf
return cons) <?> str
mkCmd1 :: Stream s m Char => String -> (a -> EsmtpCmd) -> ParsecT s u m a
-> ParsecT s u m EsmtpCmd
mkCmd1 str cons p = do
try (caseString str)
_ <- wsp
input <- getInput
st <- getState
let eol = skipMany wsp >> crlf
p' = between (many wsp) eol p <?> str
r <- lift $ runParserT p' st "" input
case r of
Left e -> return (WrongArg str e)
Right a -> return (cons a)
tokenList :: Stream s m Char => ParsecT s u m String -> Char -> ParsecT s u m String
tokenList p c = fmap (intercalate [c]) (sepBy1 p (char c))