unsubscribe

package
v1.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package unsubscribe is the HMAC-signed token primitive that backs the one-click email unsubscribe endpoints from §5.1 of the notification-preferences paper.

Token shape (base64url, no padding):

body = user_id || "|" || category || "|" || expires_at_unix
mac  = HMAC-SHA256(NOTIFY_UNSUBSCRIBE_SECRET, body)
tok  = base64url(mac || body)

The first 32 bytes of the decoded token are the MAC; the rest is the plaintext payload. Verification recomputes the MAC over the payload and constant-time-compares against the prefix. A consumed-state check is layered on top via the notification_unsubscribe_tokens collection — Sign+Verify are pure (no DB).

Why not JWT: every byte counts in a URL that has to fit in mail clients' wrap rules and in the SMS-like List-Unsubscribe-Post space. HMAC + a 3-field tuple is the smallest thing that works.

Index

Constants

View Source
const Default30Days = 30 * 24 * time.Hour

Default30Days is the standard expiry window — CAN-SPAM requires processing within 10 business days, GDPR within 30; the paper §5.1 pins 30 days.

Variables

View Source
var ErrExpired = errors.New("unsubscribe: token expired")

ErrExpired is returned for tokens whose ExpiresAt is in the past. Distinct from ErrInvalid so the route can render a "this link expired" page rather than a generic error.

View Source
var ErrInvalid = errors.New("unsubscribe: invalid token")

ErrInvalid is returned for any failed decode / HMAC mismatch / bad payload shape. Generic by design.

Functions

This section is empty.

Types

type Payload

type Payload struct {
	UserID    string
	Category  string
	ExpiresAt time.Time
}

Payload is the verified inner state. Empty Category means "unsubscribe from all marketing"; the paper §5.1 + the consent-log schema use "" for the global-unsub case.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer constructs and verifies tokens with a secret key. The key comes from NOTIFY_UNSUBSCRIBE_SECRET — fail-closed boot if missing in production mode.

func NewSigner

func NewSigner(secret string) (*Signer, error)

NewSigner returns a Signer for the given secret. Empty secret is rejected — every caller should resolve this from env at boot.

func (*Signer) Sign

func (s *Signer) Sign(userID, category string, expiresAt time.Time) (string, error)

Sign returns a base64url token for (userID, category, expiresAt). expiresAt is truncated to second precision because the token carries a unix timestamp.

func (*Signer) SignWithTTL

func (s *Signer) SignWithTTL(userID, category string, now time.Time, ttl time.Duration) (string, error)

SignWithTTL is a convenience that signs a token expiring `ttl` from now. now is injectable for tests.

func (*Signer) Verify

func (s *Signer) Verify(token string, now time.Time) (Payload, error)

Verify decodes + authenticates a token. Returns the verified Payload or an error explaining the failure mode.

Errors are intentionally non-leaking — they say "invalid" or "expired" without dumping the parsed payload, so a brute-force attacker learns no internal state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL