Skip to content

Commit cae1424

Browse files
smart constructor + top level send function
1 parent 8d80868 commit cae1424

File tree

2 files changed

+54
-10
lines changed

2 files changed

+54
-10
lines changed

azure-email/Azure/Email.hs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,39 @@ import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime)
1616
import Network.HTTP.Client.TLS (newTlsManager)
1717
import Servant.API
1818
import Servant.Client (BaseUrl (..), ClientM, Scheme (..), client, mkClientEnv, runClientM)
19-
import UnliftIO (MonadIO (..))
19+
import UnliftIO (MonadIO (..), throwString)
2020

2121
import qualified Data.ByteString as BS
2222
import qualified Data.ByteString.Base64 as B64
2323
import qualified Data.ByteString.Char8 as C8
2424
import qualified Data.Text as Text
2525

26+
{- | Send an email provided a request payload
27+
28+
Errors are thrown in IO. For a variant where error is captured
29+
in an @Left@ branch, see @sendEmailEither@
30+
-}
2631
sendEmail ::
2732
MonadIO m =>
2833
Text ->
34+
Text ->
2935
AzureEmailRequest ->
3036
m AzureEmailResponse
31-
sendEmail apiSecret payload = undefined
37+
sendEmail apiSecret emailHost payload = do
38+
resp <- sendEmailEither apiSecret emailHost payload
39+
case resp of
40+
Left err -> throwString $ show err
41+
Right r -> pure r
3242

43+
-- | Send an email provided a request payload
3344
sendEmailEither ::
3445
MonadIO m =>
3546
Text ->
47+
Text ->
3648
AzureEmailRequest ->
3749
m (Either Text AzureEmailResponse)
38-
sendEmailEither apiSecret payload = undefined
50+
sendEmailEither apiSecret emailHost payload =
51+
liftIO $ callSendEmailClient sendEmailApi payload emailHost apiSecret
3952

4053
type SendEmailApi =
4154
"emails:send"
@@ -45,17 +58,17 @@ type SendEmailApi =
4558
:> Header' '[Required, Strict] "x-ms-content-sha256" Text
4659
:> Header' '[Required, Strict] "Authorization" Text
4760
:> ReqBody '[JSON] AzureEmailRequest
48-
:> PostNoContent
61+
:> Post '[JSON] AzureEmailResponse
4962

50-
sendEmailApi :: Text -> Text -> Text -> Text -> Text -> AzureEmailRequest -> ClientM NoContent
63+
sendEmailApi :: Text -> Text -> Text -> Text -> Text -> AzureEmailRequest -> ClientM AzureEmailResponse
5164
sendEmailApi = client (Proxy @SendEmailApi)
5265

5366
callSendEmailClient ::
54-
(Text -> Text -> Text -> Text -> Text -> AzureEmailRequest -> ClientM NoContent) ->
67+
(Text -> Text -> Text -> Text -> Text -> AzureEmailRequest -> ClientM AzureEmailResponse) ->
5568
AzureEmailRequest ->
5669
Text ->
5770
Text ->
58-
IO (Either Text ())
71+
IO (Either Text AzureEmailResponse)
5972
callSendEmailClient action req azureEmailHost secret = do
6073
manager <- liftIO newTlsManager
6174
(formatToAzureTime -> now) <- getCurrentTime
@@ -79,8 +92,8 @@ callSendEmailClient action req azureEmailHost secret = do
7992
pure $ case res of
8093
Left err -> do
8194
Left . Text.pack $ show err
82-
Right _ -> do
83-
Right ()
95+
Right r -> do
96+
Right r
8497
where
8598
apiVersion :: Text
8699
apiVersion = "2023-03-31"

azure-email/Azure/Types.hs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ module Azure.Types
1313
, EmailRecipients (..)
1414
, EmailContent (..)
1515
, EmailAttachment (..)
16+
17+
-- * Smart constructors
18+
, newAzureEmailRequest
1619
) where
1720

1821
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, withText, (.:), (.=))
1922
import Data.Aeson.Types (parseFail)
2023
import Data.Text (Text)
2124

25+
import qualified Data.Text as Text
26+
2227
{- | Each email is represented as an object with @displayName@
2328
and an associated @address@.
2429
@@ -37,6 +42,14 @@ instance ToJSON EmailAddress where
3742
, "displayName" .= eaDisplayName
3843
]
3944

45+
{- | Why text type instead of represting it as @EmailAddress@?
46+
47+
Well, Azure API dictates that sender address should only be the email
48+
instead of a combination of email and display name (EmailAddress in our case).
49+
Therefore, we fallback to use text as a type alias for this one case.
50+
-}
51+
type SenderEmailAddress = Text
52+
4053
-- | Fields to represent @cc@, @bcc@ and @to@ in an email
4154
data EmailRecipients = EmailRecipients
4255
{ ccRecipients :: ![EmailAddress]
@@ -97,7 +110,7 @@ Source: https://learn.microsoft.com/en-us/rest/api/communication/dataplane/email
97110
data AzureEmailRequest = AzureEmailRequest
98111
{ aerContent :: !EmailContent
99112
, aerRecipients :: !EmailRecipients
100-
, aerSenderAddress :: !Text -- TODO: This should probably be it's own newtype
113+
, aerSenderAddress :: !SenderEmailAddress
101114
, aerReplyTo :: ![EmailAddress] -- TODO: Should this be NonEmpty instead?
102115
, aerAttachments :: ![EmailAttachment]
103116
, aerUserEngagementTrackingDisabled :: !Bool
@@ -115,6 +128,24 @@ instance ToJSON AzureEmailRequest where
115128
, "userEngagementTrackingDisabled" .= aerUserEngagementTrackingDisabled
116129
]
117130

131+
{- | Smart constructor to build a send email request.
132+
133+
There are few default settings that the caller needs to be aware of:
134+
1. @replyTo@ for recipient is the sender's email address. In case there needs to be multiple
135+
email addresses in @replyTo@ field, it is advised to build a custom request based on the
136+
exposed data types instead.
137+
2. Attachements are not included, yet.
138+
3. Enagagement tracking is disabled.
139+
-}
140+
newAzureEmailRequest ::
141+
SenderEmailAddress ->
142+
EmailRecipients ->
143+
EmailContent ->
144+
AzureEmailRequest
145+
newAzureEmailRequest senderAddress recipients content =
146+
let senderEmailAddress = EmailAddress senderAddress Text.empty
147+
in AzureEmailRequest content recipients senderAddress [senderEmailAddress] [] True
148+
118149
{- | Possible states once a send email action is triggered.
119150
Source: https://learn.microsoft.com/en-us/rest/api/communication/dataplane/email/send?view=rest-communication-dataplane-2023-03-31&tabs=HTTP#emailsendstatus
120151
-}

0 commit comments

Comments
 (0)