Skip to content
This repository was archived by the owner on Feb 7, 2025. It is now read-only.

Commit 43509fd

Browse files
author
Eric Easley
committed
Implement basic email sending type
0 parents  commit 43509fd

File tree

8 files changed

+273
-0
lines changed

8 files changed

+273
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.stack-work

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Front Row Education
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Setup.hs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

sendgrid.cabal

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: sendgrid
2+
version: 0.1.0.0
3+
synopsis: Client library for SendGrid API
4+
description: Please see README.md
5+
homepage: http://github.com/frontrowed/sendgrid#readme
6+
license: MIT
7+
license-file: LICENSE
8+
author: Eric Easley
9+
maintainer: [email protected]
10+
copyright: 2016 Front Row Education
11+
category: Network
12+
build-type: Simple
13+
-- extra-source-files:
14+
cabal-version: >=1.10
15+
16+
library
17+
hs-source-dirs: src
18+
exposed-modules: Network.API.SendGrid
19+
, Network.API.SendGrid.Types
20+
build-depends: base >= 4.7 && < 5
21+
, email-validate
22+
, bytestring
23+
, case-insensitive
24+
, these
25+
, semigroups
26+
, text
27+
, time
28+
, http-types
29+
, blaze-html
30+
, wreq
31+
, aeson
32+
, http-client
33+
, dlist
34+
default-language: Haskell2010
35+
36+
test-suite sendgrid-test
37+
type: exitcode-stdio-1.0
38+
hs-source-dirs: test
39+
main-is: Spec.hs
40+
build-depends: base
41+
, sendgrid
42+
, time
43+
, these
44+
, email-validate
45+
, semigroups
46+
, wreq
47+
ghc-options: -threaded -rtsopts -with-rtsopts=-N
48+
default-language: Haskell2010
49+
50+
source-repository head
51+
type: git
52+
location: https://github.com/frontrowed/sendgrid

src/Network/API/SendGrid.hs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module Network.API.SendGrid where

src/Network/API/SendGrid/Types.hs

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE RecordWildCards #-}
4+
5+
module Network.API.SendGrid.Types where
6+
7+
import Data.Aeson (pairs, (.=), fromEncoding, Encoding)
8+
import Data.ByteString (ByteString)
9+
import Data.ByteString.Builder as B (toLazyByteString)
10+
import Data.ByteString.Lazy as BSL (toStrict)
11+
import Data.CaseInsensitive (foldedCase)
12+
import Data.DList as D (toList, fromList)
13+
import Data.List.NonEmpty as NE (NonEmpty, toList)
14+
import Data.Maybe (maybeToList)
15+
import Data.Monoid ((<>))
16+
import Data.Text as T (Text, pack, unpack)
17+
import Data.Text.Encoding (decodeUtf8)
18+
import Data.These (These(..))
19+
import Data.Time (UTCTime(..), rfc822DateFormat, defaultTimeLocale, formatTime)
20+
import Network.HTTP.Client (RequestBody(RequestBodyBS))
21+
import Network.HTTP.Client.MultipartFormData (partFileRequestBody)
22+
import Network.HTTP.Types.Header (Header)
23+
import Network.Wreq
24+
import Network.Wreq.Types (Postable(..))
25+
import Text.Blaze.Html (Html)
26+
import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
27+
import Text.Email.Validate as E (EmailAddress, toByteString)
28+
29+
data NamedEmail
30+
= NamedEmail
31+
{ neEmail :: EmailAddress
32+
, neName :: Text
33+
} deriving (Eq, Show)
34+
35+
data File
36+
= File
37+
{ fileName :: Text
38+
, fileContent :: ByteString
39+
} deriving (Eq, Show)
40+
41+
data Content
42+
= Content
43+
{ contentFile :: File
44+
, contentId :: Text
45+
} deriving (Eq, Show)
46+
47+
data SendEmail
48+
= SendEmail
49+
{ sendTo :: Either (NonEmpty NamedEmail) (NonEmpty EmailAddress)
50+
, sendSubject :: Text
51+
, sendBody :: These Html Text
52+
, sendFrom :: EmailAddress
53+
, sendCc :: Maybe (Either (NonEmpty NamedEmail) (NonEmpty EmailAddress))
54+
, sendBcc :: Maybe (Either (NonEmpty NamedEmail) (NonEmpty EmailAddress))
55+
, sendFromName :: Maybe Text
56+
, sendReplyTo :: Maybe EmailAddress
57+
, sendDate :: Maybe UTCTime
58+
-- Don't duplicate files from `sendContent` here
59+
, sendFiles :: Maybe (NonEmpty File)
60+
, sendContent :: Maybe (NonEmpty Content)
61+
, sendHeaders :: Maybe (NonEmpty Header)
62+
}
63+
-- Can't derive (`Eq` or `Show`) because of `Html`
64+
65+
mkSendEmail :: Either (NonEmpty NamedEmail) (NonEmpty EmailAddress) -> Text -> These Html Text -> EmailAddress -> SendEmail
66+
mkSendEmail to subject body from
67+
= SendEmail
68+
{ sendTo = to
69+
, sendSubject = subject
70+
, sendBody = body
71+
, sendFrom = from
72+
, sendCc = Nothing
73+
, sendBcc = Nothing
74+
, sendFromName = Nothing
75+
, sendReplyTo = Nothing
76+
, sendDate = Nothing
77+
, sendFiles = Nothing
78+
, sendContent = Nothing
79+
, sendHeaders = Nothing
80+
}
81+
82+
emailsToParts :: Text -> Text -> Either (NonEmpty NamedEmail) (NonEmpty EmailAddress) -> [Part]
83+
emailsToParts emailKey nameKey (Left namedEmails) =
84+
flip foldMap namedEmails
85+
(\NamedEmail{..} ->
86+
[ partBS emailKey $ E.toByteString neEmail
87+
, partText nameKey neName
88+
])
89+
emailsToParts emailKey _ (Right emails) =
90+
partBS emailKey . E.toByteString <$> NE.toList emails
91+
92+
instance Postable SendEmail where
93+
postPayload = postPayload . sendEmailToParts
94+
95+
sendEmailToParts :: SendEmail -> [Part]
96+
sendEmailToParts SendEmail{..} =
97+
D.toList $ foldMap D.fromList
98+
[ toParts
99+
, [subjectPart]
100+
, bodyParts
101+
, [fromPart]
102+
, ccParts
103+
, bccParts
104+
, fromNamePart
105+
, replyToPart
106+
, datePart
107+
, headerPart
108+
, fileParts
109+
, contentParts
110+
]
111+
where
112+
fileToPart File{..} = partFileRequestBody ("files[" <> fileName <> "]") (T.unpack fileName) (RequestBodyBS fileContent)
113+
contentParts = maybe [] (foldMap contentToParts . NE.toList) sendContent
114+
where
115+
contentToParts (Content file@File{..} contentId) =
116+
[fileToPart file, partText ("content[" <> fileName <> "]") contentId]
117+
fileParts = maybe [] (map fileToPart . NE.toList) sendFiles
118+
toParts = emailsToParts "to[]" "toname[]" sendTo
119+
fromPart = partBS "from" $ E.toByteString sendFrom
120+
ccParts = maybe [] (emailsToParts "cc[]" "ccname[]") sendCc
121+
bccParts = maybe [] (emailsToParts "bcc[]" "bccname[]") sendBcc
122+
fromNamePart = maybeToList $ partText "fromname" <$> sendFromName
123+
replyToPart = maybeToList $ partBS "replyto" . E.toByteString <$> sendReplyTo
124+
datePart = maybeToList $ partText "date" . T.pack . formatTime defaultTimeLocale rfc822DateFormat <$> sendDate
125+
subjectPart = partText "subject" sendSubject
126+
headerPart = maybeToList $ partBS "headers" . encodingToByteString . encodeHeaders . NE.toList <$> sendHeaders
127+
bodyParts =
128+
case sendBody of
129+
This html -> [partBS "html" . BSL.toStrict $ renderHtml html]
130+
That text -> [partText "text" text]
131+
These html text -> [partBS "html" . BSL.toStrict $ renderHtml html, partText "text" text]
132+
133+
encodingToByteString :: Encoding -> ByteString
134+
encodingToByteString = BSL.toStrict . B.toLazyByteString . fromEncoding
135+
136+
encodeHeaders :: [Header] -> Encoding
137+
encodeHeaders = pairs . foldMap (\(key, value) -> decodeUtf8 (foldedCase key) .= decodeUtf8 value)

stack.yaml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This file was automatically generated by stack init
2+
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
3+
4+
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
5+
resolver: lts-5.8
6+
7+
# Local packages, usually specified by relative directory name
8+
packages:
9+
- '.'
10+
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
11+
extra-deps:
12+
- aeson-0.11.1.1
13+
14+
# Override default flag values for local packages and extra-deps
15+
flags: {}
16+
17+
# Extra package databases containing global packages
18+
extra-package-dbs: []
19+
20+
# Control whether we use the GHC we find on the path
21+
# system-ghc: true
22+
23+
# Require a specific version of stack, using version ranges
24+
# require-stack-version: -any # Default
25+
# require-stack-version: >= 1.0.0
26+
27+
# Override the architecture used by stack, especially useful on Windows
28+
# arch: i386
29+
# arch: x86_64
30+
31+
# Extra directories used by stack for building
32+
# extra-include-dirs: [/path/to/dir]
33+
# extra-lib-dirs: [/path/to/dir]
34+
35+
# Allow a newer minor version of GHC than the snapshot specifies
36+
# compiler-check: newer-minor

test/Spec.hs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{-# LANGUAGE OverloadedLists #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
import Data.These (These(..))
5+
import Data.Time (UTCTime(..), fromGregorian)
6+
import Network.Wreq
7+
import Text.Email.Validate as E (unsafeEmailAddress)
8+
9+
import Network.API.SendGrid.Types
10+
11+
main :: IO ()
12+
main = print =<< post "http://requestb.in/o93m97o9" example
13+
14+
example :: SendEmail
15+
example =
16+
(mkSendEmail
17+
(Left [NamedEmail (unsafeEmailAddress "foo" "foo.com") "FooFoo"])
18+
"Subject"
19+
(That "Text body")
20+
(unsafeEmailAddress "bar" "bar.com"))
21+
{ sendFiles = Just [File "fileName" "fileBody"]
22+
, sendDate = Just (UTCTime (fromGregorian 1900 1 1) 0)
23+
}

0 commit comments

Comments
 (0)