{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Network.Mail.Newsletter.Web.Handlers
( serveNewsletter
, serverNewsletterSubGet, serverNewsletterSubPost
, serverNewsletterSubConfirmGet, serverNewsletterSubConfirmPost
, serverNewsletterUnsubGet, serverNewsletterUnsubPost
) where
import Control.Lens
import Control.Monad.Error.Class
import Control.Monad.Reader
import qualified Data.Aeson as JS
import qualified Data.HashMap.Strict as HM
import Data.Machine
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time
import Network.Mail.Mime (Address(..))
import Network.Mail.Newsletter.Class
import Network.Mail.Newsletter.Web.API
import Network.Mail.Newsletter.Web.Templates
import Network.URI
import Servant
import System.Random
import Text.Blaze.Html
import qualified Web.ClientSession as CS
import Web.FormUrlEncoded
doUnsub :: (Monad m, Newsletter m) => Address -> m ()
doUnsub addr = runT_ $ source [addr] ~> unsubscribe
serveNewsletter :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,IsElem NewsletterUnsubGet (ApiRoot m)
,MonadReader r m, MonadError ServantErr m, Newsletter m
,DedupSubscriptions m, HasNewsletterTimeouts r, HasAllowedOrigins r
,MonadIO m)
=> ServerT NewsletterAPI m
serveNewsletter =
(serverNewsletterUnsubGet :<|> serverNewsletterUnsubPost)
:<|> (serverNewsletterSubGet :<|> serverNewsletterSubPost)
:<|> (serverNewsletterSubConfirmGet :<|> serverNewsletterSubConfirmPost)
serverNewsletterUnsubGet :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,MonadReader r m, MonadError ServantErr m, Newsletter m, MonadIO m)
=> Text -> m (Headers '[Header "Referrer-Policy" RefPolicy] Html)
serverNewsletterUnsubGet csAddr = addHeader StrictOrigin <$> do
mAddr <- decodeCS (Just . Address Nothing . TE.decodeUtf8) csAddr
case mAddr of
Just addr -> join $ view unsubConfirmTempl <*> pure addr
Nothing -> throwError err400
serverNewsletterUnsubPost :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,MonadReader r m, MonadError ServantErr m, Newsletter m, MonadIO m)
=> Text -> m (Headers '[Header "Referrer-Policy" RefPolicy] Html)
serverNewsletterUnsubPost csAddr = addHeader StrictOrigin <$> do
maddr <- decodeCS (Just . Address Nothing . TE.decodeUtf8) csAddr
case maddr of
Just addr -> do
doUnsub addr
join $ view unsubTempl <*> pure addr
Nothing ->
throwError err400
serverNewsletterSubGet :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,MonadReader r m, MonadError ServantErr m, Newsletter m, MonadIO m)
=> m Html
serverNewsletterSubGet = do
dk <- T.pack <$> liftIO (replicateM 5 (randomRIO ('a', 'z')))
gk <- T.pack <$> liftIO (replicateM 5 (randomRIO ('a', 'z')))
gv <- T.pack <$> liftIO (replicateM 5 (randomRIO ('a', 'z')))
k <- view clientSessionKey
f <- TE.decodeUtf8 <$> liftIO (CS.encryptIO k (TE.encodeUtf8 $ T.intercalate ":" [dk, gk, gv]))
join $ view subStartPage <*> pure f <*> pure dk <*> pure gk <*> pure gv
serverNewsletterSubPost :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,MonadReader r m, MonadError ServantErr m, Newsletter m
,DedupSubscriptions m, HasAllowedOrigins r, MonadIO m)
=> Maybe String -> Maybe String -> Form -> m Html
serverNewsletterSubPost morig mref frm = do
asrc <- view allowedOrigins
k <- view clientSessionKey
case (join $ uriAuthority <$> maybe (parseAbsoluteURI =<< mref) parseAbsoluteURI morig
,checkFormSec k) of
(Nothing, _) -> throwError err400
(Just a, Right True) | a `elem` asrc -> do
case HM.lookup "email" (unForm frm) of
Just [email] -> do
let addr = Address Nothing email
join $ when <$> recentlySubscribed addr
<*> pure (sendSubscribe addr =<< join (view optInEmail <*> pure addr))
join $ view subStartSubmitted <*> pure addr
_ -> throwError err400
_ -> throwError err403
where
checkFormSec :: CS.Key -> Either Text Bool
checkFormSec k = do
f <- parseUnique "f" frm
case (T.splitOn ":" . TE.decodeUtf8) <$> CS.decrypt k (TE.encodeUtf8 f) of
Just [dk,gk,gv] -> do
d <- (==(""::Text)) <$> parseUnique dk frm
g <- (== gv) <$> parseUnique gk frm
Right (d&&g)
_ -> pure False
serverNewsletterSubConfirmGet :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,MonadReader r m, MonadError ServantErr m, Newsletter m
,HasNewsletterTimeouts r, MonadIO m)
=> Text -> m (Headers '[Header "Referrer-Policy" RefPolicy] Html)
serverNewsletterSubConfirmGet csAddr = addHeader StrictOrigin <$> do
now <- utctDay <$> liftIO getCurrentTime
msr <- decSignup csAddr
tout <- view nltSubTimeout
case msr of
Just (SignupReq day email) | (day `diffDays` now) < tout ->
join $ view confirmStart <*> pure (Address Nothing email)
_ -> throwError err400
serverNewsletterSubConfirmPost :: (HasClientSessionKey r, HasApiRoot m, HasNlTemplates r m
,IsElem NewsletterUnsubGet (ApiRoot m)
,MonadReader r m, MonadError ServantErr m, Newsletter m
,HasNewsletterTimeouts r, MonadIO m)
=> Text -> m (Headers '[Header "Referrer-Policy" RefPolicy] Html)
serverNewsletterSubConfirmPost csAddr = addHeader StrictOrigin <$> do
now <- liftIO getCurrentTime
msr <- decSignup csAddr
tout <- (+) <$> view nltSubTimeout <*> view nltGrace
case msr of
Just (SignupReq day email) | (day `diffDays` (utctDay now)) < tout -> do
let addr = Address Nothing email
unsub <- genUnsubLink addr
runT_ $ source [(addr, JS.object [("subscribedAt", JS.toJSON now)
,("unsubscribe", JS.toJSON $ show unsub)])] ~> subscribe
join $ view confirmConfirmed <*> pure addr
_ -> throwError err400