{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DerivingStrategies #-}
module Network.HaskellNet.SMTP.Internal
( SMTPConnection(..)
, Command(..)
, SMTPException(..)
, ReplyCode
, tryCommand
, parseResponse
, sendCommand
, sendMailData
, closeSMTP
, gracefullyCloseSMTP
, quitSMTP
, Address(..)
) where
import Control.Exception
import Control.Monad (unless)
import Data.Char (isDigit)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Typeable
import Network.HaskellNet.Auth
import Network.HaskellNet.BSStream
import Network.Mail.Mime
import Prelude
data SMTPConnection = SMTPC {
SMTPConnection -> BSStream
bsstream :: !BSStream,
SMTPConnection -> [ByteString]
_response :: ![ByteString]
}
data Command
=
HELO T.Text
|
EHLO T.Text
|
MAIL T.Text
|
RCPT T.Text
|
DATA ByteString
|
EXPN T.Text
|
VRFY T.Text
|
HELP T.Text
|
AUTH AuthType UserName Password
|
NOOP
|
RSET
|
QUIT
deriving (Int -> Command -> ShowS
[Command] -> ShowS
Command -> String
(Int -> Command -> ShowS)
-> (Command -> String) -> ([Command] -> ShowS) -> Show Command
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Command] -> ShowS
$cshowList :: [Command] -> ShowS
show :: Command -> String
$cshow :: Command -> String
showsPrec :: Int -> Command -> ShowS
$cshowsPrec :: Int -> Command -> ShowS
Show, Command -> Command -> Bool
(Command -> Command -> Bool)
-> (Command -> Command -> Bool) -> Eq Command
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Command -> Command -> Bool
$c/= :: Command -> Command -> Bool
== :: Command -> Command -> Bool
$c== :: Command -> Command -> Bool
Eq)
type ReplyCode = Int
data SMTPException
=
UnexpectedReply Command [ReplyCode] ReplyCode BS.ByteString
| NotConfirmed ReplyCode BS.ByteString
| AuthNegotiationFailed ReplyCode BS.ByteString
| NoRecipients Mail
| UnexpectedGreeting ReplyCode
deriving (Int -> SMTPException -> ShowS
[SMTPException] -> ShowS
SMTPException -> String
(Int -> SMTPException -> ShowS)
-> (SMTPException -> String)
-> ([SMTPException] -> ShowS)
-> Show SMTPException
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [SMTPException] -> ShowS
$cshowList :: [SMTPException] -> ShowS
show :: SMTPException -> String
$cshow :: SMTPException -> String
showsPrec :: Int -> SMTPException -> ShowS
$cshowsPrec :: Int -> SMTPException -> ShowS
Show)
deriving (Typeable)
instance Exception SMTPException where
displayException :: SMTPException -> String
displayException (UnexpectedReply Command
cmd [Int]
expected Int
code ByteString
msg) =
String
"Cannot execute command " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Command -> String
forall a. Show a => a -> String
show Command
cmd String -> ShowS
forall a. [a] -> [a] -> [a]
++
String
", " String -> ShowS
forall a. [a] -> [a] -> [a]
++ [Int] -> String
prettyExpected [Int]
expected String -> ShowS
forall a. [a] -> [a] -> [a]
++
String
", " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> ByteString -> String
prettyReceived Int
code ByteString
msg
where
prettyReceived :: Int -> ByteString -> String
prettyReceived :: Int -> ByteString -> String
prettyReceived Int
co ByteString
ms = String
"but received" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> String
forall a. Show a => a -> String
show Int
co String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
" (" String -> ShowS
forall a. [a] -> [a] -> [a]
++ ByteString -> String
BS.unpack ByteString
ms String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
")"
prettyExpected :: [ReplyCode] -> String
prettyExpected :: [Int] -> String
prettyExpected [Int
x] = String
"expected reply code of " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> String
forall a. Show a => a -> String
show Int
x
prettyExpected [Int]
xs = String
"expected any reply code of " String -> ShowS
forall a. [a] -> [a] -> [a]
++ [Int] -> String
forall a. Show a => a -> String
show [Int]
xs
displayException (NotConfirmed Int
code ByteString
msg) =
String
"This server cannot accept any data. code: " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> String
forall a. Show a => a -> String
show Int
code String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
", msg: " String -> ShowS
forall a. [a] -> [a] -> [a]
++ ByteString -> String
BS.unpack ByteString
msg
displayException (AuthNegotiationFailed Int
code ByteString
msg) =
String
"Authentication failed. code: " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> String
forall a. Show a => a -> String
show Int
code String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
", msg: " String -> ShowS
forall a. [a] -> [a] -> [a]
++ ByteString -> String
BS.unpack ByteString
msg
displayException (NoRecipients Mail
_mail) =
String
"No recipients were specified"
displayException (UnexpectedGreeting Int
code) =
String
"Expected greeting from the server, but got: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show Int
code
tryCommand
:: SMTPConnection
-> Command
-> Int
-> [ReplyCode]
-> IO ByteString
tryCommand :: SMTPConnection -> Command -> Int -> [Int] -> IO ByteString
tryCommand SMTPConnection
conn Command
cmd Int
tries [Int]
expectedReplies = do
(Int
code, ByteString
msg) <- SMTPConnection -> Command -> IO (Int, ByteString)
sendCommand SMTPConnection
conn Command
cmd
case () of
()
_ | Int
code Int -> [Int] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Int]
expectedReplies -> ByteString -> IO ByteString
forall (m :: * -> *) a. Monad m => a -> m a
return ByteString
msg
()
_ | Int
tries Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
1 ->
SMTPConnection -> Command -> Int -> [Int] -> IO ByteString
tryCommand SMTPConnection
conn Command
cmd (Int
tries Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1) [Int]
expectedReplies
| Bool
otherwise ->
SMTPException -> IO ByteString
forall e a. Exception e => e -> IO a
throwIO (SMTPException -> IO ByteString) -> SMTPException -> IO ByteString
forall a b. (a -> b) -> a -> b
$ Command -> [Int] -> Int -> ByteString -> SMTPException
UnexpectedReply Command
cmd [Int]
expectedReplies Int
code ByteString
msg
parseResponse :: BSStream -> IO (ReplyCode, ByteString)
parseResponse :: BSStream -> IO (Int, ByteString)
parseResponse BSStream
st =
do (ByteString
code, [ByteString]
bdy) <- IO (ByteString, [ByteString])
readLines
(Int, ByteString) -> IO (Int, ByteString)
forall (m :: * -> *) a. Monad m => a -> m a
return (String -> Int
forall a. Read a => String -> a
read (String -> Int) -> String -> Int
forall a b. (a -> b) -> a -> b
$ ByteString -> String
BS.unpack ByteString
code, [ByteString] -> ByteString
BS.unlines [ByteString]
bdy)
where readLines :: IO (ByteString, [ByteString])
readLines =
do ByteString
l <- BSStream -> IO ByteString
bsGetLine BSStream
st
let (ByteString
c, ByteString
bdy) = (Char -> Bool) -> ByteString -> (ByteString, ByteString)
BS.span Char -> Bool
isDigit ByteString
l
if Bool -> Bool
not (ByteString -> Bool
BS.null ByteString
bdy) Bool -> Bool -> Bool
&& ByteString -> Char
BS.head ByteString
bdy Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'-'
then do (ByteString
c2, [ByteString]
ls) <- IO (ByteString, [ByteString])
readLines
(ByteString, [ByteString]) -> IO (ByteString, [ByteString])
forall (m :: * -> *) a. Monad m => a -> m a
return (ByteString
c2, ByteString -> ByteString
BS.tail ByteString
bdyByteString -> [ByteString] -> [ByteString]
forall a. a -> [a] -> [a]
:[ByteString]
ls)
else (ByteString, [ByteString]) -> IO (ByteString, [ByteString])
forall (m :: * -> *) a. Monad m => a -> m a
return (ByteString
c, [ByteString -> ByteString
BS.tail ByteString
bdy])
sendCommand
:: SMTPConnection
-> Command
-> IO (ReplyCode, ByteString)
sendCommand :: SMTPConnection -> Command -> IO (Int, ByteString)
sendCommand (SMTPC BSStream
conn [ByteString]
_) (DATA ByteString
dat) =
do BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn ByteString
"DATA"
(Int
code, ByteString
msg) <- BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Int
code Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
354) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ SMTPException -> IO ()
forall e a. Exception e => e -> IO a
throwIO (SMTPException -> IO ()) -> SMTPException -> IO ()
forall a b. (a -> b) -> a -> b
$ Int -> ByteString -> SMTPException
NotConfirmed Int
code ByteString
msg
(ByteString -> IO ()) -> [ByteString] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (ByteString -> IO ()
sendLine (ByteString -> IO ())
-> (ByteString -> ByteString) -> ByteString -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
stripCR) ([ByteString] -> IO ()) -> [ByteString] -> IO ()
forall a b. (a -> b) -> a -> b
$ ByteString -> [ByteString]
BS.lines ByteString
dat [ByteString] -> [ByteString] -> [ByteString]
forall a. [a] -> [a] -> [a]
++ [String -> ByteString
BS.pack String
"."]
BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
where sendLine :: ByteString -> IO ()
sendLine = BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn
stripCR :: ByteString -> ByteString
stripCR ByteString
bs = case ByteString -> Maybe (ByteString, Char)
BS.unsnoc ByteString
bs of
Just (ByteString
line, Char
'\r') -> ByteString
line
Maybe (ByteString, Char)
_ -> ByteString
bs
sendCommand (SMTPC BSStream
conn [ByteString]
_) (AUTH AuthType
LOGIN String
username String
password) =
do BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn ByteString
command
(Int
_, ByteString
_) <- BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ String -> ByteString
BS.pack String
userB64
(Int
_, ByteString
_) <- BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ String -> ByteString
BS.pack String
passB64
BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
where command :: ByteString
command = ByteString
"AUTH LOGIN"
(String
userB64, String
passB64) = String -> String -> (String, String)
login String
username String
password
sendCommand (SMTPC BSStream
conn [ByteString]
_) (AUTH AuthType
at String
username String
password) =
do BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ Text -> ByteString
T.encodeUtf8 Text
command
(Int
code, ByteString
msg) <- BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Int
code Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
334) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ SMTPException -> IO ()
forall e a. Exception e => e -> IO a
throwIO (SMTPException -> IO ()) -> SMTPException -> IO ()
forall a b. (a -> b) -> a -> b
$ Int -> ByteString -> SMTPException
AuthNegotiationFailed Int
code ByteString
msg
BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ String -> ByteString
BS.pack (String -> ByteString) -> String -> ByteString
forall a b. (a -> b) -> a -> b
$ AuthType -> String -> String -> ShowS
auth AuthType
at (ByteString -> String
BS.unpack ByteString
msg) String
username String
password
BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
where command :: Text
command = [Text] -> Text
T.unwords [Text
"AUTH", String -> Text
T.pack (AuthType -> String
forall a. Show a => a -> String
show AuthType
at)]
sendCommand (SMTPC BSStream
conn [ByteString]
_) Command
meth =
do BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
conn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$! Text -> ByteString
T.encodeUtf8 Text
command
BSStream -> IO (Int, ByteString)
parseResponse BSStream
conn
where command :: Text
command = case Command
meth of
(HELO Text
param) -> Text
"HELO " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param
(EHLO Text
param) -> Text
"EHLO " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param
(MAIL Text
param) -> Text
"MAIL FROM:<" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
">"
(RCPT Text
param) -> Text
"RCPT TO:<" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
">"
(EXPN Text
param) -> Text
"EXPN " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param
(VRFY Text
param) -> Text
"VRFY " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
param
(HELP Text
msg) -> if Text -> Bool
T.null Text
msg
then Text
"HELP\r\n"
else Text
"HELP " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
msg
Command
NOOP -> Text
"NOOP"
Command
RSET -> Text
"RSET"
Command
QUIT -> Text
"QUIT"
(DATA ByteString
_) ->
String -> Text
forall a. HasCallStack => String -> a
error String
"BUG: DATA pattern should be matched by sendCommand patterns"
(AUTH {}) ->
String -> Text
forall a. HasCallStack => String -> a
error String
"BUG: AUTH pattern should be matched by sendCommand patterns"
quitSMTP :: SMTPConnection -> IO ()
quitSMTP :: SMTPConnection -> IO ()
quitSMTP SMTPConnection
c = do
ByteString
_ <- SMTPConnection -> Command -> Int -> [Int] -> IO ByteString
tryCommand SMTPConnection
c Command
QUIT Int
1 [Int
221]
() -> IO ()
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
closeSMTP :: SMTPConnection -> IO ()
closeSMTP :: SMTPConnection -> IO ()
closeSMTP (SMTPC BSStream
conn [ByteString]
_) = BSStream -> IO ()
bsClose BSStream
conn
gracefullyCloseSMTP :: SMTPConnection -> IO ()
gracefullyCloseSMTP :: SMTPConnection -> IO ()
gracefullyCloseSMTP c :: SMTPConnection
c@(SMTPC BSStream
conn [ByteString]
_) = SMTPConnection -> IO ()
quitSMTP SMTPConnection
c IO () -> IO () -> IO ()
forall a b. IO a -> IO b -> IO a
`finally` BSStream -> IO ()
bsClose BSStream
conn
sendMailData :: Address
-> [Address]
-> ByteString
-> SMTPConnection
-> IO ()
sendMailData :: Address -> [Address] -> ByteString -> SMTPConnection -> IO ()
sendMailData Address
sender [Address]
receivers ByteString
dat SMTPConnection
conn = do
Command -> IO ByteString
sendAndCheck (Text -> Command
MAIL (Address -> Text
addressEmail Address
sender))
(Address -> IO ByteString) -> [Address] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (Command -> IO ByteString
sendAndCheck (Command -> IO ByteString)
-> (Address -> Command) -> Address -> IO ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Command
RCPT (Text -> Command) -> (Address -> Text) -> Address -> Command
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Address -> Text
addressEmail) [Address]
receivers
Command -> IO ByteString
sendAndCheck (ByteString -> Command
DATA ByteString
dat)
() -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
where
sendAndCheck :: Command -> IO ByteString
sendAndCheck Command
cmd = SMTPConnection -> Command -> Int -> [Int] -> IO ByteString
tryCommand SMTPConnection
conn Command
cmd Int
1 [Int
250, Int
251]
crlf :: BS.ByteString
crlf :: ByteString
crlf = String -> ByteString
BS.pack String
"\r\n"
bsPutCrLf :: BSStream -> ByteString -> IO ()
bsPutCrLf :: BSStream -> ByteString -> IO ()
bsPutCrLf BSStream
h ByteString
s = BSStream -> ByteString -> IO ()
bsPut BSStream
h (ByteString
s ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
crlf)