sendgrid.go 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. package webhooks
  2. import (
  3. "crypto/ecdsa"
  4. "crypto/sha256"
  5. "crypto/x509"
  6. "encoding/asn1"
  7. "encoding/base64"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "math/big"
  12. "strings"
  13. "time"
  14. "github.com/knadh/listmonk/models"
  15. )
  16. type sendgridNotif struct {
  17. Email string `json:"email"`
  18. Timestamp int64 `json:"timestamp"`
  19. Event string `json:"event"`
  20. }
  21. // Sendgrid handles Sendgrid/SNS webhook notifications including confirming SNS topic subscription
  22. // requests and bounce notifications.
  23. type Sendgrid struct {
  24. pubKey *ecdsa.PublicKey
  25. }
  26. // NewSendgrid returns a new Sendgrid instance.
  27. func NewSendgrid(key string) (*Sendgrid, error) {
  28. // Get the certificate from the key.
  29. sigB, err := base64.StdEncoding.DecodeString(key)
  30. if err != nil {
  31. return nil, err
  32. }
  33. pubKey, err := x509.ParsePKIXPublicKey(sigB)
  34. if err != nil {
  35. return nil, err
  36. }
  37. return &Sendgrid{pubKey: pubKey.(*ecdsa.PublicKey)}, nil
  38. }
  39. // ProcessBounce processes Sendgrid bounce notifications and returns one or more Bounce objects.
  40. func (s *Sendgrid) ProcessBounce(sig, timestamp string, b []byte) ([]models.Bounce, error) {
  41. if err := s.verifyNotif(sig, timestamp, b); err != nil {
  42. return nil, err
  43. }
  44. var notifs []sendgridNotif
  45. if err := json.Unmarshal(b, &notifs); err != nil {
  46. return nil, fmt.Errorf("error unmarshalling Sendgrid notification: %v", err)
  47. }
  48. out := make([]models.Bounce, 0, len(notifs))
  49. for _, n := range notifs {
  50. if n.Event != "bounce" {
  51. continue
  52. }
  53. tstamp := time.Unix(n.Timestamp, 0)
  54. b := models.Bounce{
  55. Email: strings.ToLower(n.Email),
  56. Type: models.BounceTypeHard,
  57. Meta: json.RawMessage(b),
  58. Source: "sendgrid",
  59. CreatedAt: tstamp,
  60. }
  61. out = append(out, b)
  62. }
  63. return out, nil
  64. }
  65. // verifyNotif verifies the signature on a notification payload.
  66. func (s *Sendgrid) verifyNotif(sig, timestamp string, b []byte) error {
  67. sigB, err := base64.StdEncoding.DecodeString(sig)
  68. if err != nil {
  69. return err
  70. }
  71. ecdsaSig := struct {
  72. R *big.Int
  73. S *big.Int
  74. }{}
  75. if _, err := asn1.Unmarshal(sigB, &ecdsaSig); err != nil {
  76. return fmt.Errorf("error asn1 unmarshal of signature: %v", err)
  77. }
  78. h := sha256.New()
  79. h.Write([]byte(timestamp))
  80. h.Write(b)
  81. hash := h.Sum(nil)
  82. if !ecdsa.Verify(s.pubKey, hash, ecdsaSig.R, ecdsaSig.S) {
  83. return errors.New("invalid signature")
  84. }
  85. return nil
  86. }