{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} module Network.Mail.Mailgun.Config ( DomainName , MailgunConfig(..) , HasMailgunConfig(..) , mailgunGetConfig , mailgunFromEnv, mailgunFromIni , MailgunConfigException(..) , _MailgunApiKeyRequired, _MailgunDomainRequired , _MailgunInvalidRegion, _MailgunIniNotFound , _MailgunConextUnavailable ) where import Control.Applicative import Control.Lens import Control.Monad.Catch import Control.Monad.Trans import Control.Monad.Trans.Maybe import qualified Data.ByteString as BS import Data.Foldable import Data.Ini import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import System.Environment import System.FilePath data MailgunConfigException = MailgunApiKeyRequired | MailgunDomainRequired | MailgunInvalidRegion | MailgunIniNotFound | MailgunConextUnavailable deriving (Show) instance Exception MailgunConfigException makePrisms ''MailgunConfigException type DomainName = String -- | The configuration we use when accessing the Mailgun API. data MailgunConfig = MailgunConfig { _mailgunDomain :: DomainName -- ^ The domain we're using mailgun with. , _mailgunApiKey :: BS.ByteString -- ^ Our mailgun API key. , _mailgunApiDomain :: String -- ^ The base URL for the mailgun API, usually "https://api.mailgun.net" , _mailgunTestMode :: Bool } deriving (Show, Eq) makeClassy ''MailgunConfig -- | The domain for the API URL used for the US region. usApiDomain :: String usApiDomain = "api.mailgun.net" -- | The domain for the API URL used for the EU region. euApiDomain :: String euApiDomain = "api.eu.mailgun.net" -- | Uses the available options to discover a MaingunConfig if possible. mailgunGetConfig :: (MonadIO m, MonadCatch m) => m MailgunConfig mailgunGetConfig = do ec <- runMaybeT . msum . map (\act -> act `catch` (\(_::MailgunConfigException) -> MaybeT $ pure Nothing)) $ [ mailgunFromEnv , mailgunFromIni ] maybe (throwM MailgunConextUnavailable) pure ec -- | Builds a MaingunConfig from enviromental variables. -- -- MAILGUN_API_KEY: Required; the API key for mailgun. -- MAILGUN_DOMAIN: Required; the domain in mailgun we're using. -- MAILGUN_REGION: Optional; Selects teh regional API endpoint. -- Valid values are 'US', and 'EU', defaults to 'US'. -- MAILGUN_API_BASE: Optional; Override the base URL (primarily for testing). -- Takes presidence over MAILGUN_REGION. -- MAILGUN_LIVE: Optional: Unless set to True, set to test mode. -- In test mode Mailgun accepts but does not send messages. mailgunFromEnv :: (MonadIO m, MonadThrow m) => m MailgunConfig mailgunFromEnv = do apiKey <- maybe (throwM MailgunApiKeyRequired) (pure . TE.encodeUtf8 . T.pack) =<< liftIO (lookupEnv "MAILGUN_API_KEY") domain <- maybe (throwM MailgunDomainRequired) pure =<< liftIO (lookupEnv "MAILGUN_DOMAIN") testmode <- ((Just "True")==) <$> liftIO (lookupEnv "MAILGUN_DOMAIN") apiDomain <- liftIO (lookupEnv "MAILGUN_API_BASE") >>= \case Just ab -> pure ab Nothing -> liftIO (lookupEnv "MAILGUN_REGION") >>= \case Nothing -> pure usApiDomain Just "US" -> pure usApiDomain Just "EU" -> pure euApiDomain _ -> throwM MailgunInvalidRegion pure $ MailgunConfig domain apiKey apiDomain testmode -- | Looks for an ini format file at ".mailgun" and "~/.mailgun" in that order. -- Credentials are read from the ini in the format: -- @ -- [mailgun] -- region: US -- domain: mydomain.com -- key: 3ax6xnjp29jd6fds4gc373sgvjxteol0 -- api_domain: api.mailgun.com -- live: True -- @ -- -- The API key and domain are required, other values are optional. mailgunFromIni :: forall m . (MonadIO m, MonadThrow m) => m MailgunConfig mailgunFromIni = do ini <- (maybe (throwM MailgunIniNotFound) pure . asum) =<< liftIO (sequence [ readIniFileMay ".mailgun" , lookupEnv "HOME" >>= maybe (pure Nothing) (\h -> readIniFileMay (h ".mailgun")) ]) apiKey <- TE.encodeUtf8 . T.pack <$> lookupMailgun MailgunApiKeyRequired ini "key" domain <- lookupMailgun MailgunApiKeyRequired ini "domain" let testmode = (Just "True") == lookupMailgunMay ini "live" let apiDomain = fromMaybe usApiDomain $ (lookupMailgunMay ini "api_domain") <|> ((\case "US" -> usApiDomain "EU" -> euApiDomain _ -> usApiDomain) <$> (lookupMailgunMay ini "region")) pure $ MailgunConfig domain apiKey apiDomain testmode where readIniFileMay :: FilePath -> IO (Maybe Ini) readIniFileMay fp = (maybeRight <$> readIniFile fp) `catchIOError` (const (pure Nothing)) lookupMailgun :: MailgunConfigException -> Ini -> Text -> m String lookupMailgun e ini key = maybe (throwM e) pure $ lookupMailgunMay ini key lookupMailgunMay :: Ini -> Text -> Maybe String lookupMailgunMay ini key = either (const Nothing) (Just . T.unpack) $ lookupValue "mailgun" key ini maybeRight :: Either a b -> Maybe b maybeRight (Left _) = Nothing maybeRight (Right b) = Just b