choonkeat / elm-aws / AWS.SES

Implementation of https://docs.aws.amazon.com/ses/latest/DeveloperGuide/using-ses-api-requests.html


type OutgoingMail
    = RawEmail ({ from : String, destinations : List String, rawMessage : String })
    | Email ({ from : String, to : List String, replyTo : List String, subject : String, textBody : String, htmlBody : String })


type Response
    = Error ({ type_ : String, code : String, message : String })
    | Success ({ messageId : String, requestId : String })

Response from SES API

See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/using-ses-api-responses.html

unsignedRequest : OutgoingMail -> Result String (AWS.Types.UnsignedRequest Http.Error Response)

Construct an UnsignedRequest for SES, e.g.

import Http
import AWS.Types

unsignedResult : Result String (AWS.Types.UnsignedRequest Http.Error Response)
unsignedResult =
    unsignedRequest
        (Email
            { from = "alice@example.com"
            , to = [ "bob@example.com" ]
            , replyTo = [ "donotreply@example.com" ]
            , subject = "Test"
            , textBody = "Message sent using SendEmail"
            , htmlBody = "<p>Message sent using SendEmail</p>"
            }
        )

Result.map .method unsignedResult
--> Ok "POST"

Result.map .headers unsignedResult
--> Ok [("Content-Type","application/x-www-form-urlencoded")]

Result.map .stringBody unsignedResult
--> Ok "Action=SendEmail&Source=alice%40example.com&Message.Subject.Data=Test&Message.Body.Text.Data=Message%20sent%20using%20SendEmail&Message.Body.Html.Data=%3Cp%3EMessage%20sent%20using%20SendEmail%3C%2Fp%3E&Destination.ToAddresses.member.1=bob%40example.com&ReplyToAddresses.member.1=donotreply%40example.com"

Result.map .service unsignedResult
--> Ok AWS.Types.ServiceSES


usage config now unsignedResult =
    unsignedResult
        |> Result.andThen (AWS.signRequest config now)
        |> Result.map Http.task


type Notification
    = SESBounce Mail Bounce
    | SESComplaint Mail Complaint
    | SESDelivery Mail Delivery

The top-level JSON object in an Amazon SES notification https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#top-level-json-object

decodeNotification : Json.Decode.Decoder Notification

decoder of an Amazon SES notification according to https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html

Inner types and decoders


type alias Mail =
{ timestamp : Time.Posix
, messageId : String
, source : String
, sourceArn : String
, sourceIp : String
, sendingAccountId : String
, destination : List String
, headersTruncated : Basics.Bool
, headers : List NamedValue
, commonHeaders : CommonMailHeaders 
}

mail in top-level JSON object https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mail-object

decodeMail : Json.Decode.Decoder Mail

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mail-object


type alias CommonMailHeaders =
{ from : List String
, date : Maybe Time.Posix
, to : List String
, messageId : Maybe String
, subject : Maybe String 
}

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mail-object

decodeCommonMailHeaders : Json.Decode.Decoder CommonMailHeaders

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mail-object


type alias Bounce =
{ bounceType : BounceType
, bouncedRecipients : List BounceRecipient
, timestamp : Time.Posix
, feedbackId : String
, remoteMtaIp : Maybe String
, reportingMTA : Maybe String 
}

bounce in top-level JSON object https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-object


type alias BounceRecipient =
{ emailAddress : String
, action : Maybe String
, status : Maybe String
, diagnosticCode : Maybe String 
}

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounced-recipients


type BounceType
    = BounceUndetermined
    | BouncePermanent
    | BouncePermanentGeneral
    | BouncePermanentNoEmail
    | BouncePermanentSuppressed
    | BouncePermanentOnAccountSuppressionList
    | BounceTransient
    | BounceTransientGeneral
    | BounceTransientMailboxFull
    | BounceTransientMessageTooLarge
    | BounceTransientContentRejected
    | BounceTransientAttachmentRejected

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types


type alias ComplainedRecipient =
{ emailAddress : String }

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complained-recipients


type alias Complaint =
{ userAgent : Maybe String
, complaintFeedbackType : Maybe ComplaintFeedbackType
, arrivalDate : Maybe Time.Posix
, complainedRecipients : List ComplainedRecipient
, timestamp : Time.Posix
, feedbackId : String
, complaintSubType : Maybe ComplaintSubType 
}

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object


type ComplaintFeedbackType
    = ComplaintFeedbackAbuse
    | ComplaintFeedbackAuthFailure
    | ComplaintFeedbackFraud
    | ComplaintFeedbackNotSpam
    | ComplaintFeedbackVirus
    | ComplaintFeedbackOther
    | ComplaintFeedbackUnknown String

complaint in top-level JSON object https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object


type ComplaintSubType
    = OnAccountSuppressionList

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object


type alias Delivery =
{ timestamp : Time.Posix
, processingTimeMillis : Basics.Int
, recipients : List String 
}

delivery in top-level JSON object https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#delivery-object


type alias NamedValue =
{ name : String
, value : String 
}

Mail headers like

  {
     "name":"From",
     "value":"\"Sender Name\" <sender@example.com>"
  }

decodeBounce : Json.Decode.Decoder Bounce

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-object

decodeBounceRecipient : Json.Decode.Decoder BounceRecipient

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounced-recipients

decodeBounceType : Json.Decode.Decoder BounceType

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types

decodeComplainedRecipient : Json.Decode.Decoder ComplainedRecipient

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complained-recipients

decodeComplaint : Json.Decode.Decoder Complaint

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object

decodeComplaintFeedbackType : Json.Decode.Decoder ComplaintFeedbackType

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object

decodeComplaintSubType : Json.Decode.Decoder ComplaintSubType

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object

decodeDelivery : Json.Decode.Decoder Delivery

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#delivery-object

decodeNamedValue : Json.Decode.Decoder NamedValue

decoder for Mail headers

Tested internals

paramsForMail : OutgoingMail -> Result String (List ( String, String ))

givenRawMessage : String
givenRawMessage =
    -- NOTE: the leading space of this string will be removed
    "\n\nFrom:user@example.com\nSubject: Test\n\nMessage sent using SendRawEmail.\n\n"

expectedRawParams : List (String, String)
expectedRawParams =
    [ ("Action", "SendRawEmail")
    , ( "Source", "charlie@example.com" )
    , ("Destinations.member.1", "alice@example.com")
    , ("Destinations.member.2", "bob@example.com")
    , ("RawMessage.Data", "RnJvbTp1c2VyQGV4YW1wbGUuY29tClN1YmplY3Q6IFRlc3QKCk1lc3NhZ2Ugc2VudCB1c2luZyBTZW5kUmF3RW1haWwuCgo=")
    ]


paramsForMail
    (RawEmail
        { from = "charlie@example.com"
        , destinations = ["alice@example.com", "bob@example.com"]
        , rawMessage = givenRawMessage
        }
    )
--> Ok expectedRawParams

givenEmailDetail : { from : String, to : List String, replyTo : List String, subject : String, textBody : String, htmlBody : String}
givenEmailDetail =
    { from = "alice@example.com"
    , to = [ "bob@example.com" ]
    , replyTo = [ "donotreply@example.com" ]
    , subject = "Test"
    , textBody = "Message sent using SendEmail"
    , htmlBody = "<p>Message sent using SendEmail</p>"
    }

expectedEmailParams : List (String, String)
expectedEmailParams =
    [ ( "Action", "SendEmail" )
    , ( "Source", "alice@example.com" )
    , ( "Message.Subject.Data", "Test" )
    , ( "Message.Body.Text.Data", "Message sent using SendEmail" )
    , ( "Message.Body.Html.Data", "<p>Message sent using SendEmail</p>" )
    , ( "Destination.ToAddresses.member.1", "bob@example.com" )
    , ( "ReplyToAddresses.member.1", "donotreply@example.com" )
    ]

paramsForMail (Email givenEmailDetail)
--> Ok expectedEmailParams

decodeResponse : Xml.Decode.Decoder Response

import Xml.Decode

--
-- Success scenario
"""
<SendEmailResponse xmlns="https://email.amazonaws.com/doc/2010-03-31/">
  <SendEmailResult>
    <MessageId>000001271b15238a-fd3ae762-2563-11df-8cd4-6d4e828a9ae8-000000</MessageId>
  </SendEmailResult>
  <ResponseMetadata>
    <RequestId>fd3ae762-2563-11df-8cd4-6d4e828a9ae8</RequestId>
  </ResponseMetadata>
</SendEmailResponse>
"""
|> Xml.Decode.run decodeResponse
--> Ok (Success  { messageId = "000001271b15238a-fd3ae762-2563-11df-8cd4-6d4e828a9ae8-000000", requestId = "fd3ae762-2563-11df-8cd4-6d4e828a9ae8" })

--
-- Error scenario
"""
<ErrorResponse>
   <Error>
      <Type>
         Sender
      </Type>
      <Code>
         ValidationError
      </Code>
      <Message>
         Value null at 'message.subject' failed to satisfy constraint: Member must not be null
      </Message>
   </Error>
   <RequestId>
      42d59b56-7407-4c4a-be0f-4c88daeea257
   </RequestId>
</ErrorResponse>
"""
|> Xml.Decode.run decodeResponse
--> Ok (Error  { type_ = "Sender", code = "ValidationError", message = "Value null at 'message.subject' failed to satisfy constraint: Member must not be null" })