AtlatestRepositorysigil-ses

sigil-ses / tree / src / amazon / sesnotify.sgl

1;;; (amazon ses notify) - SNS notification parsing for SES bounce/complaint events.
2;;;
3;;; SES sends bounce and complaint notifications via SNS as JSON POST
4;;; requests to a webhook URL. This module provides stateless parsers
5;;; that extract notification type, affected email addresses, and metadata.
6
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 ;; Records
16 ses-notification
17 ses-notification?
18 ses-notification-type
19 ses-notification-addresses
20 ses-notification-bounce-type
21 ses-notification-bounce-sub-type
22 ses-notification-complaint-type
23 ses-notification-timestamp
24 ses-notification-feedback-id
25 ses-notification-raw
27 ;; Parsing
28 parse-ses-notification
30 ;; Predicates
31 ses-bounce?
32 ses-hard-bounce?
33 ses-complaint?)
35 (begin
37 ;; ---------------------------------------------------------------
38 ;; Notification record
39 ;; ---------------------------------------------------------------
41 (define-struct ses-notification
42 (type) ; "Bounce" or "Complaint"
43 (addresses) ; list of affected email addresses
44 (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 timestamp
48 (feedback-id) ; SES feedback ID for tracking
49 (raw)) ; original parsed dict for full access
51 ;; ---------------------------------------------------------------
52 ;; Parsing
53 ;; ---------------------------------------------------------------
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->list
60 (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 type
67 ;;; 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 (cond
72 ((string=? notif-type "Bounce")
73 (let ((bounce (dict-ref parsed bounce: #{})))
74 (ses-notification
75 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: #f
80 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-notification
87 type: "Complaint"
88 addresses: (extract-addresses complaint complainedRecipients:)
89 bounce-type: #f
90 bounce-sub-type: #f
91 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 predicates
100 ;; ---------------------------------------------------------------
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")))))