AtlatestRepositorysigil-ses
1
;;; (amazon ses notify) - SNS notification parsing for SES bounce/complaint events.2
;;;3
;;; SES sends bounce and complaint notifications via SNS as JSON POST4
;;; requests to a webhook URL. This module provides stateless parsers5
;;; that extract notification type, affected email addresses, and metadata.7
(define-library (amazon ses notify)8
(import (sigil core)9
(sigil dict)10
(sigil string)11
(sigil struct)12
(sigil json)13
(only (sigil array) array-map))15
(export ;; Records16
ses-notification17
ses-notification?18
ses-notification-type19
ses-notification-addresses20
ses-notification-bounce-type21
ses-notification-bounce-sub-type22
ses-notification-complaint-type23
ses-notification-timestamp24
ses-notification-feedback-id25
ses-notification-raw27
;; Parsing28
parse-ses-notification30
;; Predicates31
ses-bounce?32
ses-hard-bounce?33
ses-complaint?)35
(begin37
;; ---------------------------------------------------------------38
;; Notification record39
;; ---------------------------------------------------------------41
(define-struct ses-notification42
(type) ; "Bounce" or "Complaint"43
(addresses) ; list of affected email addresses44
(bounce-type) ; "Permanent", "Transient", etc. (bounces only)45
(bounce-sub-type) ; "General", "NoEmail", etc. (bounces only)46
(complaint-type) ; "abuse", "not-spam", etc. (complaints only)47
(timestamp) ; ISO 8601 timestamp48
(feedback-id) ; SES feedback ID for tracking49
(raw)) ; original parsed dict for full access51
;; ---------------------------------------------------------------52
;; Parsing53
;; ---------------------------------------------------------------55
;;; Extract email addresses from a recipient array in a notification dict.56
(define (extract-addresses dict key)57
(let ((recipients (dict-ref dict key #[])))58
(if (array? recipients)59
(array->list60
(array-map (lambda (r) (dict-ref r emailAddress: ""))61
recipients))62
'())))64
;;; Parse an SNS notification JSON dict into an ses-notification record.65
;;; Accepts either a pre-parsed dict or a JSON string.66
;;; Returns an ses-notification record, or #f if the notification type67
;;; is not recognized (e.g., "Delivery" notifications).68
(define (parse-ses-notification data)69
(let ((parsed (if (string? data) (json-decode data) data)))70
(let ((notif-type (dict-ref parsed notificationType: #f)))71
(cond72
((string=? notif-type "Bounce")73
(let ((bounce (dict-ref parsed bounce: #{})))74
(ses-notification75
type: "Bounce"76
addresses: (extract-addresses bounce bouncedRecipients:)77
bounce-type: (dict-ref bounce bounceType: "")78
bounce-sub-type: (dict-ref bounce bounceSubType: "")79
complaint-type: #f80
timestamp: (dict-ref bounce timestamp:81
(dict-ref parsed timestamp: ""))82
feedback-id: (dict-ref bounce feedbackId: "")83
raw: parsed)))84
((string=? notif-type "Complaint")85
(let ((complaint (dict-ref parsed complaint: #{})))86
(ses-notification87
type: "Complaint"88
addresses: (extract-addresses complaint complainedRecipients:)89
bounce-type: #f90
bounce-sub-type: #f91
complaint-type: (dict-ref complaint complaintFeedbackType: "")92
timestamp: (dict-ref complaint timestamp:93
(dict-ref parsed timestamp: ""))94
feedback-id: (dict-ref complaint feedbackId: "")95
raw: parsed)))96
(else #f)))))98
;; ---------------------------------------------------------------99
;; Convenience predicates100
;; ---------------------------------------------------------------102
;;; True if the notification is a bounce.103
(define (ses-bounce? notif)104
(and (ses-notification? notif)105
(string=? (ses-notification-type notif) "Bounce")))107
;;; True if the notification is a permanent (hard) bounce.108
(define (ses-hard-bounce? notif)109
(and (ses-bounce? notif)110
(string=? (ses-notification-bounce-type notif) "Permanent")))112
;;; True if the notification is a complaint.113
(define (ses-complaint? notif)114
(and (ses-notification? notif)115
(string=? (ses-notification-type notif) "Complaint")))))