123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- package webhooks
- import (
- "crypto/ecdsa"
- "crypto/sha256"
- "crypto/x509"
- "encoding/asn1"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "math/big"
- "strings"
- "time"
- "github.com/knadh/listmonk/models"
- )
- type sendgridNotif struct {
- Email string `json:"email"`
- Timestamp int64 `json:"timestamp"`
- Event string `json:"event"`
- }
- // Sendgrid handles Sendgrid/SNS webhook notifications including confirming SNS topic subscription
- // requests and bounce notifications.
- type Sendgrid struct {
- pubKey *ecdsa.PublicKey
- }
- // NewSendgrid returns a new Sendgrid instance.
- func NewSendgrid(key string) (*Sendgrid, error) {
- // Get the certificate from the key.
- sigB, err := base64.StdEncoding.DecodeString(key)
- if err != nil {
- return nil, err
- }
- pubKey, err := x509.ParsePKIXPublicKey(sigB)
- if err != nil {
- return nil, err
- }
- return &Sendgrid{pubKey: pubKey.(*ecdsa.PublicKey)}, nil
- }
- // ProcessBounce processes Sendgrid bounce notifications and returns one or more Bounce objects.
- func (s *Sendgrid) ProcessBounce(sig, timestamp string, b []byte) ([]models.Bounce, error) {
- if err := s.verifyNotif(sig, timestamp, b); err != nil {
- return nil, err
- }
- var notifs []sendgridNotif
- if err := json.Unmarshal(b, ¬ifs); err != nil {
- return nil, fmt.Errorf("error unmarshalling Sendgrid notification: %v", err)
- }
- out := make([]models.Bounce, 0, len(notifs))
- for _, n := range notifs {
- if n.Event != "bounce" {
- continue
- }
- tstamp := time.Unix(n.Timestamp, 0)
- b := models.Bounce{
- Email: strings.ToLower(n.Email),
- Type: models.BounceTypeHard,
- Meta: json.RawMessage(b),
- Source: "sendgrid",
- CreatedAt: tstamp,
- }
- out = append(out, b)
- }
- return out, nil
- }
- // verifyNotif verifies the signature on a notification payload.
- func (s *Sendgrid) verifyNotif(sig, timestamp string, b []byte) error {
- sigB, err := base64.StdEncoding.DecodeString(sig)
- if err != nil {
- return err
- }
- ecdsaSig := struct {
- R *big.Int
- S *big.Int
- }{}
- if _, err := asn1.Unmarshal(sigB, &ecdsaSig); err != nil {
- return fmt.Errorf("error asn1 unmarshal of signature: %v", err)
- }
- h := sha256.New()
- h.Write([]byte(timestamp))
- h.Write(b)
- hash := h.Sum(nil)
- if !ecdsa.Verify(s.pubKey, hash, ecdsaSig.R, ecdsaSig.S) {
- return errors.New("invalid signature")
- }
- return nil
- }
|