{-# LANGUAGE DeriveDataTypeable #-}

-- | An SMTP client in the IO Monad.
--
-- Data structures for representing SMTP status codes and email messages are
-- re-exported here from /Text.ParserCombinators.Parsec.Rfc2821/ and
-- /Text.ParserCombinators.Parsec.Rfc2822/ in the /hsemail/ package.
--
-- Here's a working example:
--
-- > import Network.SMTP.ClientSession
-- > import Network.SMTP.Client
-- > import Network.Socket
-- > import System.Time
-- > import System.IO
-- > import Data.IORef
-- > 
-- > myDomain = "example.com"
-- > smtpHost = "mail.example.com"    -- <-- Your SMTP server here
-- > 
-- > main = do
-- >     now <- getClockTime
-- >     nowCT <- toCalendarTime now
-- >     let message = Message [
-- >                 From [NameAddr (Just "Mr. Nobody") "nobody@example.com"],
-- >                 To   [NameAddr (Just "Mr. Somebody") "somebody@example.com"],
-- >                 Subject "I'm using SMTPClient!",
-- >                 Date nowCT
-- >             ]
-- >             ("Dear Sir,\n"++
-- >              "It has come to my attention that this is an email.\n"++
-- >              "Yours sincerely,\n"++
-- >              "Mr. Nobody\n")
-- >     addrs <- getAddrInfo Nothing (Just smtpHost) (Just "25")
-- >     putStrLn $ "connecting to "++show (map addrAddress addrs)
-- >     sentRef <- newIORef []
-- >     sendSMTP' (hPutStrLn stderr) (Just sentRef) myDomain addrs [message]
-- >     statuses <- readIORef sentRef
-- >     -- If no exception was caught, statuses is guaranteed to be
-- >     -- the same length as the list of input messages, therefore head won't fail here.
-- >     case head statuses of
-- >         Nothing     -> putStrLn "Message successfully sent"
-- >         Just status -> putStrLn $ "Message send failed with status "++show status

module Network.SMTP.Client (
        sendSMTP,
        sendSMTP',
        processSMTP,
        SMTPException(..),
        SmtpReply(..),
        SmtpCode(..),
        SuccessCode(..),
        Category(..),
        Message,
        GenericMessage(..),
        Field(..),
        NameAddr(..)
    ) where

import Network.SMTP.ClientSession
import Control.Exception.Extensible
import Control.Monad
import Text.ParserCombinators.Parsec.Rfc2821 (
        SmtpReply(..),
        SmtpCode(..),
        SuccessCode(..),
        Category(..)
    )
import Text.ParserCombinators.Parsec.Rfc2822 (
        Message(..),
        GenericMessage(Message),
        Field(..),
        NameAddr(..)
    )
import Network.Socket
import Control.Applicative
import System.IO
import Control.Monad
import Data.Typeable
import Data.IORef
import Data.List (foldl1')


-- | Send a list of email messages to an SMTP server. Throws SMTPException on
-- failure at the communication protocol level, and it can also throw
-- socket-level exceptions.
--
-- The optional IORef is used to store a list of statuses for messages sent so
-- far, where Nothing means success.  The list elements correspond to the elements
-- of the input message list.  If the caller catches an exception, this list is
-- likely to be shorter than the input message list:  The length of the list
-- indicates how many messages were dispatched.  If no exception is caught, the
-- length of the statuses will equal the length of the input messages list.
--
-- The message body may use either \"\\n\" or \"\\r\\n\" as an end-of-line
-- marker and in either case it will be sent correctly to the server.
sendSMTP ::
            Maybe (IORef [Maybe SmtpReply]) -- ^ For storing failure statuses of messages sent so far
         -> String      -- ^ Domain name for EHLO command
         -> [AddrInfo]  -- ^ Network addresses of SMTP server (will try each in turn)
         -> [Message]   -- ^ List of messages to send
         -> IO ()
sendSMTP = sendSMTP' (\_ -> return ())

-- | Like sendSMTP but takes an additional function for logging all input and
-- output for diagnostic purposes.
sendSMTP' :: (String -> IO ())  -- ^ Diagnostic log function
          -> Maybe (IORef [Maybe SmtpReply]) -- ^ For storing failure statuses of messages sent so far
          -> String             -- ^ Domain name for EHLO command
          -> [AddrInfo]         -- ^ Network addresses of SMTP server (will try each in turn)
          -> [Message]          -- ^ List of messages to send
          -> IO ()
sendSMTP' log mStatuses domain addrs messages = do
        eHandle <- try . foldl1' mplus . map (
                \addr -> bracketOnError
                    (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
                    sClose
                    (\sock -> do
                            connect sock (addrAddress addr)
                            socketToHandle sock ReadWriteMode
                        )
            ) $ addrs
        case eHandle of
            Left exc -> throwIO (exc :: IOException)
            Right handle -> do
                let smtp = smtpClientSession domain messages
                processSMTP log mStatuses handle smtp
              `finally` hClose handle

-- | A lower level function that does the I/O processing for an SMTP client session on a handle.
-- Returns when the session has completed, with the handle still open.
processSMTP :: (String -> IO ())               -- ^ Diagnostic log function
            -> Maybe (IORef [Maybe SmtpReply]) -- ^ For storing failure statuses of messages sent so far
            -> Handle
            -> SMTPState -> IO ()
processSMTP log mStatuses h state = do

    case mStatuses of
        Just statuses -> writeIORef statuses (smtpStatuses state)
        Nothing -> return ()

    forM_ (smtpOutQueue state) $ \line -> do
        log $ "-> "++line
        hPutStr h line
        hPutStr h "\r\n"
    hFlush h

    case (smtpSuccess state, smtpFailure state) of
        (True, _) -> do
            log "SUCCEEDED"
        (False, Just err) ->
            throwIO $ SMTPException err
        otherwise -> do
            -- Strip trailing \r. hGetLine has already stripped \n for us.
            reply <- reverse . dropWhile (=='\r') . reverse <$> hGetLine h
            log $ "<- "++reply
            let state' = smtpReceive state reply $ state {smtpOutQueue = []}
            processSMTP log mStatuses h state'

-- | An exception indicating a communications failure at the level of the SMTP protocol.
data SMTPException = SMTPException String
    deriving (Eq, Show, Typeable)

instance Exception SMTPException where