{-# LANGUAGE DeriveAnyClass #-} -- | SMTP is s an incredibly stable and well supported protocol. -- Using this rather then API's prevents vendorlocking. -- -- This module provides a ready to go connection pool for SMTP. -- Which has been used in various deployments already. module Network.Mail.Pool ( sendEmail , smtpPool , defSettings , SmtpCred(..) , PoolSettings(..) -- ** specify connection type , openTls , openPlain , openTls' -- ** optparse applicative , emailOptions -- ** lenses , poolCred , poolConnf , poolStripes , poolUnused , poolStripeMax , smtpHost , smtpLogin , smtpPassword , smtpPort -- * Exceptions , ServiceAuthFailure -- * re exports , module X ) where import Control.Exception import Control.Monad.IO.Class import Data.Aeson import Data.Pool as X import Data.Time (NominalDiffTime) import Lens.Micro import Network.HaskellNet.SMTP as X import Network.HaskellNet.SMTP.SSL as X import Network.Mail.Mime import Network.Socket import Options.Applicative import Type.Reflection (Typeable) -- | Failed to authetnicate with some upstream service (smtp for example) newtype ServiceAuthFailure a = ServiceAuthFailure a deriving (Typeable, Show) deriving anyclass Exception -- | Authentication information for the SMTP connection data SmtpCred = SmtpCred { _smtpPassword :: String , _smtpLogin :: String , _smtpHost :: String , _smtpPort :: PortNumber } deriving (Show) instance FromJSON SmtpCred where parseJSON = withObject "SmtpCred" $ \v -> SmtpCred <$> v .: "password" <*> v .: "login" <*> v .: "host" <*> (fromInteger <$> v .: "port") smtpHost :: Lens' SmtpCred String smtpHost = lens _smtpHost (\a b -> a{_smtpHost= b}) smtpLogin :: Lens' SmtpCred String smtpLogin = lens _smtpLogin (\a b -> a{_smtpLogin= b}) smtpPassword :: Lens' SmtpCred String smtpPassword = lens _smtpPassword (\a b -> a{_smtpPassword= b}) smtpPort :: Lens' SmtpCred PortNumber smtpPort = lens _smtpPort (\a b -> a{_smtpPort= b}) -- | This allows you to override the default settings from 'defSettings' data PoolSettings = PoolSettings { -- | credentials for smtp connection _poolCred :: SmtpCred -- | allows overriding of the opening function, for example 'openPlain' or 'openTls' , _poolConnf :: SmtpCred -> IO SMTPConnection -- | stripes, see docs: https://hackage.haskell.org/package/resource-pool-0.2.3.2/docs/Data-Pool.html , _poolStripes :: Int -- | specify how long connections are kept open. , _poolUnused :: NominalDiffTime -- | how many connections per stripe. , _poolStripeMax :: Int } poolCred :: Lens' PoolSettings SmtpCred poolCred = lens _poolCred (\a b -> a{_poolCred=b}) poolConnf :: Lens' PoolSettings (SmtpCred -> IO SMTPConnection) poolConnf = lens _poolConnf (\a b -> a{_poolConnf=b}) poolStripes :: Lens' PoolSettings Int poolStripes = lens _poolStripes (\a b -> a{_poolStripes=b}) poolUnused :: Lens' PoolSettings NominalDiffTime poolUnused = lens _poolUnused (\a b -> a{_poolUnused=b}) poolStripeMax :: Lens' PoolSettings Int poolStripeMax = lens _poolStripeMax (\a b -> a{_poolStripeMax=b}) -- | Create settings with good defaults from 'SmtpCred'. defSettings :: SmtpCred -> PoolSettings defSettings cred = PoolSettings { _poolCred = cred , _poolConnf = openPlain , _poolStripes = 1 , _poolUnused = 60 , _poolStripeMax = 5 } openPlain :: SmtpCred -> IO SMTPConnection openPlain smtp = connectSMTPPort (smtp ^. smtpHost) (smtp ^. smtpPort) openTls :: SmtpCred -> IO SMTPConnection openTls = openTls' defaultSettingsSMTPSTARTTLS openTls' :: Settings -> SmtpCred -> IO SMTPConnection openTls' def smtp = connectSMTPSTARTTLSWithSettings (smtp ^. smtpHost) $ def { sslPort = smtp ^. smtpPort } -- | Construct a connection pool from settings. smtpPool :: PoolSettings -> IO (Pool SMTPConnection) smtpPool smtp = createPool (do conn <- smtp ^. poolConnf $ smtp ^. poolCred authorize conn (smtp ^. poolCred) pure conn ) closeSMTP (smtp ^. poolStripes) (smtp ^. poolUnused) 5 handleAny :: (SomeException -> IO a) -> IO a -> IO a handleAny = handle -- | we need to auth only once per connection. -- this is annoying because we want to crash on failure to auth. authorize :: SMTPConnection -> SmtpCred -> IO () authorize conn smtp = do handleAny (\ex -> do closeSMTP conn -- don't leak throwIO ex) $ do isSuccess <- authenticate LOGIN (smtp ^. smtpLogin) (smtp ^. smtpPassword) conn if isSuccess then pure () else throwIO $ ServiceAuthFailure $ smtpPassword .~ "obfuscated, see the running instance CLI" $ smtp emailOptions :: Parser SmtpCred emailOptions = SmtpCred <$> strOption (long "smtp-pass" <> metavar "SMTP-PASS" <> help "the smtp password, in case of mailjet: https://app.mailjet.com/transactional/smtp") <*> strOption (long "smtp-login" <> metavar "SMTP-LOGIN" <> help "the smtp login name, in case of mailjet: https://app.mailjet.com/transactional/smtp") <*> strOption (long "smtp-host" <> metavar "SMTP-HOST" <> value "in-v3.mailjet.com" <> showDefault <> help "the smtp host, excluding port") <*> option auto (long "smtp-port" <> help "The port on which the smtp server listens" <> showDefault <> value 587 <> metavar "SMTP-PORT") -- | Send a 'Mail' with help of a connection pool. sendEmail :: MonadIO m => Pool SMTPConnection -> Mail -> m () sendEmail pool = liftIO . withResource pool . sendMimeMail2