Move Yesod.Mail.Send to a new dedicated separate library
This commit is contained in:
parent
865d81c235
commit
ff5bb97383
3 changed files with 2 additions and 235 deletions
|
@ -1,234 +0,0 @@
|
||||||
{- This file is part of Vervis.
|
|
||||||
-
|
|
||||||
- Written in 2018 by fr33domlover <fr33domlover@riseup.net>.
|
|
||||||
-
|
|
||||||
- ♡ Copying is an act of love. Please copy, reuse and share.
|
|
||||||
-
|
|
||||||
- The author(s) have dedicated all copyright and related and neighboring
|
|
||||||
- rights to this software to the public domain worldwide. This software is
|
|
||||||
- distributed without any warranty.
|
|
||||||
-
|
|
||||||
- You should have received a copy of the CC0 Public Domain Dedication along
|
|
||||||
- with this software. If not, see
|
|
||||||
- <http://creativecommons.org/publicdomain/zero/1.0/>.
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- | This modules provides email support for Yesod apps. It allows handler code
|
|
||||||
-- to send email messages, synchronously (i.e. instantly in the same thread)
|
|
||||||
-- and asynchronously (i.e. pass the work to a separate thread, so that the
|
|
||||||
-- user can have their HTTP response without waiting for the mail to be sent).
|
|
||||||
--
|
|
||||||
-- Since the module is based on my own usage, some simple things aren't
|
|
||||||
-- provided, but can be trivially provided if someone needs them (or when I get
|
|
||||||
-- to the task of adding them regardless, whichever happens first):
|
|
||||||
--
|
|
||||||
-- * Only plain text email is supported, but HTML email support is trivial to
|
|
||||||
-- add if someone needs it
|
|
||||||
-- * Only a single recipient is taken per message, but it's trivial to support
|
|
||||||
-- taking a list of recipients
|
|
||||||
-- * The mail is sent via an SMTP server using the @smtp-mail@ package. However
|
|
||||||
-- it's easy to add flexibility to choose some other method. For example
|
|
||||||
-- sending via the sendmail executable (package @mime-mail@), or sending via
|
|
||||||
-- the Amazon SES server (package @mime-mail-ses@).
|
|
||||||
module Yesod.Mail.Send
|
|
||||||
( YesodMailSend (..)
|
|
||||||
, MailSettings ()
|
|
||||||
, MailRecipe ()
|
|
||||||
, Address (..)
|
|
||||||
, sendMail
|
|
||||||
, submitMail
|
|
||||||
, runMailer
|
|
||||||
)
|
|
||||||
where
|
|
||||||
|
|
||||||
import Prelude
|
|
||||||
|
|
||||||
import Control.Concurrent.Chan (writeChan)
|
|
||||||
import Control.Monad (forever)
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import Control.Monad.Logger
|
|
||||||
import Control.Monad.Trans.Class (lift)
|
|
||||||
import Control.Monad.Trans.Reader (ReaderT (runReaderT), ask)
|
|
||||||
import Data.Aeson
|
|
||||||
import Data.Text (Text)
|
|
||||||
import Data.Text.Encoding (encodeUtf8)
|
|
||||||
import Database.Persist.Sql (LogFunc)
|
|
||||||
import Network.Mail.Mime (Mail, simpleMail')
|
|
||||||
import Network.Mail.SMTP hiding (Address (..), sendMail)
|
|
||||||
import Network.Socket (HostName, PortNumber)
|
|
||||||
import Text.Email.Validate (EmailAddress, validate)
|
|
||||||
import Text.Shakespeare.Text (TextUrl, renderTextUrl)
|
|
||||||
import Yesod.Core (Route, Yesod)
|
|
||||||
import Yesod.Core.Handler (HandlerT {-HandlerFor-}, getsYesod, getUrlRenderParams)
|
|
||||||
|
|
||||||
import qualified Network.Mail.Mime as M (Address (..))
|
|
||||||
|
|
||||||
import Text.Email.Local
|
|
||||||
|
|
||||||
type HandlerFor site = HandlerT site IO
|
|
||||||
|
|
||||||
class Yesod site => YesodMailSend site where
|
|
||||||
|
|
||||||
-- |
|
|
||||||
data MailMessage site
|
|
||||||
|
|
||||||
-- |
|
|
||||||
formatMailMessage
|
|
||||||
:: Bool
|
|
||||||
-> Maybe Text
|
|
||||||
-> MailMessage site
|
|
||||||
-> (Text, TextUrl (Route site))
|
|
||||||
|
|
||||||
-- |
|
|
||||||
getMailSettings
|
|
||||||
:: HandlerFor site (Maybe MailSettings)
|
|
||||||
|
|
||||||
-- |
|
|
||||||
getSubmitMail
|
|
||||||
:: HandlerFor site (Maybe (MailRecipe site -> HandlerFor site ()))
|
|
||||||
|
|
||||||
data SmtpLogin = SmtpLogin
|
|
||||||
{ smtpUser :: String
|
|
||||||
, smtpPassword :: String
|
|
||||||
}
|
|
||||||
|
|
||||||
instance FromJSON SmtpLogin where
|
|
||||||
parseJSON = withObject "SmtpLogin" $ \ o ->
|
|
||||||
SmtpLogin
|
|
||||||
<$> o .: "user"
|
|
||||||
<*> o .: "password"
|
|
||||||
|
|
||||||
data SmtpSettings = SmtpSettings
|
|
||||||
{ smtpLogin :: Maybe SmtpLogin
|
|
||||||
, smtpHost :: HostName
|
|
||||||
, smtpPort :: PortNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
instance FromJSON SmtpSettings where
|
|
||||||
parseJSON = withObject "SmtpSettings" $ \ o ->
|
|
||||||
SmtpSettings
|
|
||||||
<$> o .:? "login"
|
|
||||||
<*> o .: "host"
|
|
||||||
<*> (fromInteger <$> o .: "port")
|
|
||||||
|
|
||||||
data EmailAddress' = EmailAddress' { toEmailAddress :: EmailAddress }
|
|
||||||
|
|
||||||
instance FromJSON EmailAddress' where
|
|
||||||
parseJSON = withText "EmailAddress" $ \ t ->
|
|
||||||
case validate $ encodeUtf8 t of
|
|
||||||
Left err -> fail $ "Parsing email address failed: " ++ err
|
|
||||||
Right email -> return $ EmailAddress' email
|
|
||||||
|
|
||||||
data Address = Address
|
|
||||||
{ addressName :: Maybe Text
|
|
||||||
, addressEmail :: EmailAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
data Address' = Address' { toAddress :: Address }
|
|
||||||
|
|
||||||
instance FromJSON Address' where
|
|
||||||
parseJSON = withObject "Address" $ \ o -> fmap Address' $
|
|
||||||
Address
|
|
||||||
<$> o .:? "name"
|
|
||||||
<*> (toEmailAddress <$> o .: "email")
|
|
||||||
|
|
||||||
data MailSettings = MailSettings
|
|
||||||
{ mailSmtp :: SmtpSettings
|
|
||||||
, mailSender :: Address
|
|
||||||
, mailAllowReply :: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
instance FromJSON MailSettings where
|
|
||||||
parseJSON = withObject "MailSettings" $ \ o ->
|
|
||||||
MailSettings
|
|
||||||
<$> o .: "smtp"
|
|
||||||
<*> (toAddress <$> o .: "sender")
|
|
||||||
<*> o .: "allow-reply"
|
|
||||||
|
|
||||||
-- | This is exported from 'Text.Shakespeare' but the docs there say it's an
|
|
||||||
-- internal module that will be hidden on the next release. So I prefer not to
|
|
||||||
-- rely on it and define this type here.
|
|
||||||
type RenderUrl url = url -> [(Text, Text)] -> Text
|
|
||||||
|
|
||||||
data MailRecipe site = MailRecipe
|
|
||||||
{ mailUrlRender :: RenderUrl (Route site)
|
|
||||||
, mailRecipient :: Address
|
|
||||||
, mailMessage :: MailMessage site
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mailer = LoggingT IO
|
|
||||||
--type Mailer = LoggingT (ReaderT ConnectionPool IO)
|
|
||||||
--type MailerDB = SqlPersistT Mailer
|
|
||||||
|
|
||||||
src :: Text
|
|
||||||
src = "Mail"
|
|
||||||
|
|
||||||
{-
|
|
||||||
runMailerDB :: MailerDB a -> Mailer a
|
|
||||||
runMailerDB action = do
|
|
||||||
pool <- lift ask
|
|
||||||
runSqlPool action pool
|
|
||||||
-}
|
|
||||||
|
|
||||||
renderMessage
|
|
||||||
:: YesodMailSend site => Address -> Bool -> MailRecipe site -> Mail
|
|
||||||
renderMessage from reply (MailRecipe render to msg) =
|
|
||||||
let (subject, mkbody) = formatMailMessage reply (addressName to) msg
|
|
||||||
conv (Address n e) = M.Address n $ emailText e
|
|
||||||
in simpleMail' (conv to) (conv from) subject $ renderTextUrl render mkbody
|
|
||||||
|
|
||||||
smtp :: SmtpSettings -> Mail -> IO ()
|
|
||||||
smtp (SmtpSettings mlogin host port) =
|
|
||||||
case mlogin of
|
|
||||||
Nothing -> sendMail' host port
|
|
||||||
Just (SmtpLogin user pass) -> sendMailWithLogin' host port user pass
|
|
||||||
|
|
||||||
send :: YesodMailSend site => MailSettings -> MailRecipe site -> IO ()
|
|
||||||
send (MailSettings s a r) = smtp s . renderMessage a r
|
|
||||||
|
|
||||||
-- | Send an email message through an SMTP server and return once it's sent.
|
|
||||||
-- Returns 'True' if sent, 'False' if email is disabled in settings.
|
|
||||||
sendMail
|
|
||||||
:: YesodMailSend site
|
|
||||||
=> Address
|
|
||||||
-> MailMessage site
|
|
||||||
-> HandlerFor site Bool
|
|
||||||
sendMail recip msg = do
|
|
||||||
msettings <- getMailSettings
|
|
||||||
case msettings of
|
|
||||||
Nothing -> return False
|
|
||||||
Just settings -> do
|
|
||||||
urp <- getUrlRenderParams
|
|
||||||
let recipe = MailRecipe urp recip msg
|
|
||||||
liftIO $ send settings recipe >> return True
|
|
||||||
|
|
||||||
-- | Submit an email message into the queue for delivery through an SMTP
|
|
||||||
-- server, and return without waiting for it to be sent. Returns 'True' if
|
|
||||||
-- submitted, 'False' if email is disabled in settings.
|
|
||||||
submitMail
|
|
||||||
:: YesodMailSend site
|
|
||||||
=> Address
|
|
||||||
-> MailMessage site
|
|
||||||
-> HandlerFor site Bool
|
|
||||||
submitMail recip msg = do
|
|
||||||
msubmit <- getSubmitMail
|
|
||||||
case msubmit of
|
|
||||||
Nothing -> return False
|
|
||||||
Just submit -> do
|
|
||||||
urp <- getUrlRenderParams
|
|
||||||
let recipe = MailRecipe urp recip msg
|
|
||||||
submit recipe >> return True
|
|
||||||
|
|
||||||
-- | Run mailer loop which reads messages from a queue and sends them to SMTP
|
|
||||||
-- server.
|
|
||||||
runMailer
|
|
||||||
:: YesodMailSend site
|
|
||||||
=> MailSettings -- ^ Details of SMTP server and email formatting
|
|
||||||
-- -> ConnectionPool -- ^ DB connection pool for DB access
|
|
||||||
-> LogFunc -- ^ What to do with log messages
|
|
||||||
-> IO (MailRecipe site) -- ^ IO action that reads a message for sending
|
|
||||||
-> IO ()
|
|
||||||
runMailer settings {-pool-} logFunc readMail =
|
|
||||||
flip {-runReaderT pool $ flip-} runLoggingT logFunc $ do
|
|
||||||
$logInfoS src "Mailer component starting"
|
|
||||||
forever $ liftIO $ readMail >>= send settings
|
|
|
@ -19,6 +19,7 @@ packages:
|
||||||
git: https://dev.seek-together.space/s/fr33domlover/r/yesod-auth-account
|
git: https://dev.seek-together.space/s/fr33domlover/r/yesod-auth-account
|
||||||
commit: 1bd49ddf91521bbfeb811af430d0e6918276d127
|
commit: 1bd49ddf91521bbfeb811af430d0e6918276d127
|
||||||
extra-dep: true
|
extra-dep: true
|
||||||
|
- '../yesod-mail-send'
|
||||||
|
|
||||||
# Packages to be pulled from upstream that are not in the resolver (e.g.,
|
# Packages to be pulled from upstream that are not in the resolver (e.g.,
|
||||||
# acme-missiles-0.3)
|
# acme-missiles-0.3)
|
||||||
|
|
|
@ -101,7 +101,6 @@ library
|
||||||
Yesod.Auth.Unverified
|
Yesod.Auth.Unverified
|
||||||
Yesod.Auth.Unverified.Creds
|
Yesod.Auth.Unverified.Creds
|
||||||
Yesod.Auth.Unverified.Internal
|
Yesod.Auth.Unverified.Internal
|
||||||
Yesod.Mail.Send
|
|
||||||
Yesod.Paginate.Local
|
Yesod.Paginate.Local
|
||||||
Yesod.SessionEntity
|
Yesod.SessionEntity
|
||||||
|
|
||||||
|
@ -316,6 +315,7 @@ library
|
||||||
, yesod-auth-account
|
, yesod-auth-account
|
||||||
, yesod-core
|
, yesod-core
|
||||||
, yesod-form
|
, yesod-form
|
||||||
|
, yesod-mail-send
|
||||||
, yesod-static
|
, yesod-static
|
||||||
, yesod-persistent
|
, yesod-persistent
|
||||||
-- for reading gzipped darcs inventory via utils in
|
-- for reading gzipped darcs inventory via utils in
|
||||||
|
|
Loading…
Reference in a new issue