@@ -0,0 +1,146 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"source.offmarket.win/aleagle/eaglecast/models"
|
||||
)
|
||||
|
||||
type lettermintNotif struct {
|
||||
ID string `json:"id"`
|
||||
Event string `json:"event"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Data struct {
|
||||
MessageID string `json:"message_id"`
|
||||
Subject string `json:"subject"`
|
||||
Recipient string `json:"recipient"`
|
||||
Response *struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
EnhancedStatus string `json:"enhanced_status_code"`
|
||||
Content string `json:"content"`
|
||||
} `json:"response"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
Tag string `json:"tag"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Lettermint handles bounce webhook notifications from Lettermint.
|
||||
type Lettermint struct {
|
||||
hmacKey []byte
|
||||
}
|
||||
|
||||
// NewLettermint returns a new Lettermint webhook handler.
|
||||
func NewLettermint(key []byte) *Lettermint {
|
||||
return &Lettermint{hmacKey: key}
|
||||
}
|
||||
|
||||
// ProcessBounce processes an incoming Lettermint webhook payload and returns a bounce object.
|
||||
func (l *Lettermint) ProcessBounce(sig string, body []byte) ([]models.Bounce, error) {
|
||||
if len(l.hmacKey) == 0 {
|
||||
return nil, fmt.Errorf("webhook key is not configured")
|
||||
}
|
||||
|
||||
// Parse the signature header: t={timestamp},v1={hex_signature}.
|
||||
ts, sigHex, err := parseLettermintSignature(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify timestamp tolerance (300 seconds).
|
||||
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
|
||||
return nil, fmt.Errorf("signature timestamp expired")
|
||||
}
|
||||
|
||||
// Decode the hex signature from the header.
|
||||
sigB, err := hex.DecodeString(strings.TrimSpace(sigHex))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid signature encoding: %v", err)
|
||||
}
|
||||
|
||||
// Compute HMAC-SHA256 of "{timestamp}.{body}" and compare.
|
||||
mac := hmac.New(sha256.New, l.hmacKey)
|
||||
mac.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
|
||||
|
||||
if !hmac.Equal(mac.Sum(nil), sigB) {
|
||||
return nil, fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
var n lettermintNotif
|
||||
if err := json.Unmarshal(body, &n); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling Lettermint notification: %v", err)
|
||||
}
|
||||
|
||||
// Map event to bounce type.
|
||||
var typ string
|
||||
switch n.Event {
|
||||
case "message.hard_bounced":
|
||||
typ = models.BounceTypeHard
|
||||
case "message.soft_bounced":
|
||||
typ = models.BounceTypeSoft
|
||||
case "message.spam_complaint":
|
||||
typ = models.BounceTypeComplaint
|
||||
default:
|
||||
// Ignore irrelevant events (e.g. webhook.test).
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
campUUID := ""
|
||||
if len(n.Data.Metadata) > 0 {
|
||||
var meta map[string]string
|
||||
if err := json.Unmarshal(n.Data.Metadata, &meta); err == nil {
|
||||
if v, ok := meta["X-EagleCast-Campaign"]; ok {
|
||||
campUUID = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t, _ := time.Parse(time.RFC3339, n.Timestamp)
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
}
|
||||
|
||||
return []models.Bounce{{
|
||||
Email: strings.ToLower(n.Data.Recipient),
|
||||
CampaignUUID: campUUID,
|
||||
Type: typ,
|
||||
Source: "lettermint",
|
||||
Meta: json.RawMessage(body),
|
||||
CreatedAt: t,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// parseLettermintSignature parses a signature header of the form "t={timestamp},v1={hex}".
|
||||
func parseLettermintSignature(sig string) (int64, string, error) {
|
||||
var (
|
||||
ts int64
|
||||
hash string
|
||||
)
|
||||
|
||||
for _, part := range strings.Split(sig, ",") {
|
||||
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
switch kv[0] {
|
||||
case "t":
|
||||
if _, err := fmt.Sscanf(kv[1], "%d", &ts); err != nil {
|
||||
return 0, "", fmt.Errorf("invalid timestamp in signature: %v", err)
|
||||
}
|
||||
case "v1":
|
||||
hash = kv[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ts == 0 || hash == "" {
|
||||
return 0, "", fmt.Errorf("invalid signature format")
|
||||
}
|
||||
|
||||
return ts, hash, nil
|
||||
}
|
||||
Reference in New Issue
Block a user