mirror of
				https://github.com/knadh/listmonk.git
				synced 2025-10-26 00:16:18 +08:00 
			
		
		
		
	- Add support for `complaint` to the SES bounce processor. - Add support for `hard/soft` to Sendgrid bounce processor. - Add new bounce actions `None` and `Unsubscribe`. - Add per type (`soft/hard/complaint`) bounce rule configuration to admin settings UI. - Refactor Cypress bounce tests.
		
			
				
	
	
		
			115 lines
		
	
	
	
		
			2.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			115 lines
		
	
	
	
		
			2.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"`
 | |
| 	BounceClassification string `json:"bounce_classification"`
 | |
| 
 | |
| 	// SendGrid flattens all X-headers and adds them to the bounce
 | |
| 	// event notification.
 | |
| 	CampaignUUID string `json:"XListmonkCampaign"`
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 		}
 | |
| 
 | |
| 		typ := models.BounceTypeHard
 | |
| 		if n.BounceClassification == "technical" || n.BounceClassification == "content" {
 | |
| 			typ = models.BounceTypeSoft
 | |
| 		}
 | |
| 
 | |
| 		tstamp := time.Unix(n.Timestamp, 0)
 | |
| 		bn := models.Bounce{
 | |
| 			CampaignUUID: n.CampaignUUID,
 | |
| 			Email:        strings.ToLower(n.Email),
 | |
| 			Type:         typ,
 | |
| 			Meta:         json.RawMessage(b),
 | |
| 			Source:       "sendgrid",
 | |
| 			CreatedAt:    tstamp,
 | |
| 		}
 | |
| 		out = append(out, bn)
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| }
 |