lists

package
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package lists manages newsletter lists and their audience: contacts, memberships with double opt-in and stored consent proof, unsubscribes, suppression after bounces and complaints, and import and export.

Index

Constants

View Source
const (
	ConfirmCooldown = time.Hour
	MaxConfirms     = 3
	ConfirmWindow   = 30 * 24 * time.Hour
)

ConfirmCooldown is the least time between two confirmation mails to one address for one list; MaxConfirms caps them per address over ConfirmWindow, across all lists.

View Source
const (
	SoftBounceLimit  = 3
	SoftBounceWindow = 14 * 24 * time.Hour
)

Soft bounce limits: an address whose deliveries keep failing temporarily is gone in practice. M3AAWG removes it after two consecutive bounces over two weeks or more, Spamhaus after three in a row (lfr:E054, lfr:E057).

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

ConfirmValidity is how long a confirmation link works.

View Source
const DefaultMinGap = 20 * time.Hour

DefaultMinGap keeps two issues of a list from landing on the same day by accident; a list that sends daily sets it lower.

Variables

View Source
var ErrExpired = errors.New("this confirmation link has expired; sign up again")

ErrExpired means a confirmation link is too old.

View Source
var ErrOptedOut = errors.New("unsubscribed, complained, bounced or erased; only a new signup with confirmation adds it again")

ErrOptedOut means an address left, complained, bounced or was erased, so only a new double opt-in may add it.

Functions

func ConsentsToTracking

func ConsentsToTracking(consent string) bool

ConsentsToTracking reports whether a consent text tells readers about open and click measurement. Only readers who agreed to such a text are measured (TDDDG §25, GDPR Art. 6).

Types

type Contact

type Contact struct {
	Email      string    `json:"email"`
	Name       string    `json:"name,omitempty"`
	Created    time.Time `json:"created"`
	Suppressed string    `json:"suppressed,omitempty"`
	Lists      []*Member `json:"lists"`
	Sent       int       `json:"sent"`
	Opens      int       `json:"opens"`
	Clicks     int       `json:"clicks"`
}

Contact is an address with its memberships.

type ImportResult

type ImportResult struct {
	Added     int      `json:"added"`
	Existing  int      `json:"existing"`
	OptedOut  int      `json:"opted_out"` // left, complained, bounced or erased: kept off
	Reconfirm int      `json:"reconfirm"` // on the list already, asked to confirm again
	Pending   []int64  `json:"-"`         // memberships that need a confirmation mail
	Invalid   []string `json:"invalid,omitempty"`
}

ImportResult counts an import.

type Input

type Input struct {
	Name, Description, From, FromName, ReplyTo, Host, Language *string
	Tracking                                                   *bool
	ImprintURL, PrivacyURL, Address, Consent, Cadence          *string
	Headline, Pitch                                            *string
	MinGap                                                     *time.Duration
}

Input sets list fields; nil pointers keep the current value.

type List

type List struct {
	ID          int64  `json:"id"`
	DomainID    int64  `json:"-"`
	Domain      string `json:"domain"`
	Slug        string `json:"slug"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	From        string `json:"from"` // sender address
	FromName    string `json:"from_name"`
	ReplyTo     string `json:"reply_to,omitempty"`
	Host        string `json:"host,omitempty"`
	Language    string `json:"language"`
	Tracking    bool   `json:"tracking"`
	ImprintURL  string `json:"imprint_url,omitempty"`
	PrivacyURL  string `json:"privacy_url,omitempty"`
	Address     string `json:"address,omitempty"`
	Consent     string `json:"consent,omitempty"`
	Paused      string `json:"paused,omitempty"`
	Cadence     string `json:"cadence,omitempty"`  // how often mail comes, shown at signup
	Headline    string `json:"headline,omitempty"` // the website's first line; the name when empty
	Pitch       string `json:"pitch,omitempty"`    // under the headline; the description when empty

	MinGap       Seconds   `json:"min_gap"` // time that must pass between two issues
	Created      time.Time `json:"created"`
	Confirmed    int       `json:"confirmed"`
	Pending      int       `json:"pending"`
	Unsubscribed int       `json:"unsubscribed"`

	// Texts is loaded with the list in the same query.
	Texts map[string]string `json:"texts,omitempty"`
	// contains filtered or unexported fields
}

List is one newsletter.

func (*List) ConsentText

func (l *List) ConsentText() string

ConsentText is the statement a signup agrees to.

func (*List) Text added in v0.2.2

func (l *List) Text(key string) string

Text resolves a reader-facing key using this list's overrides.

func (*List) TextValues added in v0.2.2

func (l *List) TextValues() map[string]TextValue

TextValues returns all effective reader texts.

type Member

type Member struct {
	ID         int64     `json:"id"`
	ListID     int64     `json:"-"`
	List       string    `json:"list"`
	ContactID  int64     `json:"contact_id"`
	Email      string    `json:"email"`
	Name       string    `json:"name,omitempty"`
	Status     string    `json:"status"`
	Source     string    `json:"source,omitempty"`
	Consent    string    `json:"consent,omitempty"`
	SignupAt   time.Time `json:"signup_at"`
	SignupIP   string    `json:"signup_ip,omitempty"`
	ConfirmAt  time.Time `json:"confirm_at,omitzero"`
	ConfirmIP  string    `json:"confirm_ip,omitempty"`
	UnsubAt    time.Time `json:"unsubscribed_at,omitzero"`
	UnsubHow   string    `json:"unsubscribed_how,omitempty"`
	Suppressed string    `json:"suppressed,omitempty"` // why the address gets nothing at all
}

Member is one address on one list.

type Query

type Query struct {
	Status string // pending, confirmed, unsubscribed; empty for all
	Search string // part of the address
	Limit  int
	After  int64
}

Query selects members.

type Seconds

type Seconds int64

Seconds is a duration stored and shown in whole seconds.

func (Seconds) Duration

func (s Seconds) Duration() time.Duration

Duration converts to time.Duration.

type Service

type Service struct {
	DB     *store.DB
	Events *events.Bus
	Now    func() time.Time
}

Service is the list store.

func (*Service) Add

func (s *Service) Add(ctx context.Context, l *List, email, name string, confirmed bool, in Signup) (*Member, bool, error)

Add puts an address on a list directly. confirmed asserts that consent exists outside lfr (e.g. a migrated list); otherwise it is a normal pending signup and the second return value says to send a confirmation.

func (*Service) All

func (s *Service) All(ctx context.Context) ([]*List, error)

All returns every list.

func (*Service) ByHost

func (s *Service) ByHost(ctx context.Context, host string) (*List, error)

ByHost finds the list whose website is host, without member counts.

func (*Service) ByID

func (s *Service) ByID(ctx context.Context, id int64) (*List, error)

ByID loads a list without member counts.

func (*Service) CheckHealth

func (s *Service) CheckHealth(ctx context.Context, listID int64) (string, error)

CheckHealth pauses a list whose complaint or bounce rate over the last 30 days is too high. It returns the reason when it paused the list.

func (*Service) Confirm

func (s *Service) Confirm(ctx context.Context, memberID int64, ip string) (*Member, error)

Confirm completes a double opt-in. Confirming twice is harmless.

func (*Service) ConfirmsLastHour

func (s *Service) ConfirmsLastHour(ctx context.Context, listID int64) int

ConfirmsLastHour counts confirmation mails a list sent in the last hour.

func (*Service) Contact

func (s *Service) Contact(ctx context.Context, email string) (*Contact, error)

Contact shows everything lfr knows about an address.

func (*Service) Create

func (s *Service) Create(ctx context.Context, slug string, in Input) (*List, error)

Create adds a list sending from an address of a served domain.

func (*Service) Engaged

func (s *Service) Engaged(ctx context.Context, contactID int64)

Engaged clears a contact's soft bounces: mail reached them.

func (*Service) ExportCSV

func (s *Service) ExportCSV(ctx context.Context, slug string, w io.Writer) (int, error)

ExportCSV writes the members of a list with their consent records.

func (*Service) Find

func (s *Service) Find(ctx context.Context, slug, email string) (*Member, error)

Find loads the membership of email on a list.

func (*Service) Forget

func (s *Service) Forget(ctx context.Context, email string) error

Forget erases an address and everything recorded about it (GDPR art. 17).

func (*Service) Get

func (s *Service) Get(ctx context.Context, slug string) (*List, error)

Get loads a list by slug.

func (*Service) Hosts

func (s *Service) Hosts(ctx context.Context, domainID int64) ([]string, error)

Hosts returns the web hosts of lists on a domain (or all when domainID is 0).

func (*Service) ImportCSV

func (s *Service) ImportCSV(ctx context.Context, l *List, r io.Reader, confirmed bool, in Signup) (ImportResult, error)

ImportCSV reads addresses (first column, or a column named email; an optional name column) and adds them. Without confirmed every address gets a confirmation request instead.

func (*Service) MarkConfirmSent

func (s *Service) MarkConfirmSent(ctx context.Context, memberID int64)

MarkConfirmSent records that a confirmation mail was queued.

func (*Service) Member

func (s *Service) Member(ctx context.Context, id int64) (*Member, error)

Member loads a membership by ID.

func (*Service) Members

func (s *Service) Members(ctx context.Context, slug string, q Query) ([]*Member, error)

Members lists the members of a list.

func (*Service) Pause

func (s *Service) Pause(ctx context.Context, slug, reason string) (*List, error)

Pause stops sending for a list; an empty reason resumes it.

func (*Service) Remove

func (s *Service) Remove(ctx context.Context, slug string) error

Remove deletes a list with its memberships and issues. Contacts stay.

func (*Service) SetTexts added in v0.2.2

func (s *Service) SetTexts(ctx context.Context, slug string, values map[string]string) (*List, error)

SetTexts validates the whole batch before applying it atomically.

func (*Service) SoftBounce

func (s *Service) SoftBounce(ctx context.Context, contactID int64, reason string) (suppressed bool, err error)

SoftBounce counts a temporary failure that ended a delivery to a contact and suppresses the address once the limits are reached.

func (*Service) Subscribe

func (s *Service) Subscribe(ctx context.Context, l *List, email string, in Signup) (*Member, bool, error)

Subscribe records a signup. It returns the membership and whether a confirmation mail should go out now. Signing up again while confirmed changes nothing; while pending it resends the confirmation after the cooldown; after unsubscribing it starts over with a new confirmation.

func (*Service) Suppress

func (s *Service) Suppress(ctx context.Context, email, reason string) error

Suppress stops all mail to an address, on every list.

func (*Service) Unsubscribe

func (s *Service) Unsubscribe(ctx context.Context, memberID, issueID int64, how string) (*Member, error)

Unsubscribe ends a membership; how records the channel (link, header, mail, operator).

func (*Service) Unsuppress

func (s *Service) Unsuppress(ctx context.Context, email string) error

Unsuppress lets an address receive mail again.

func (*Service) Update

func (s *Service) Update(ctx context.Context, slug string, in Input) (*List, error)

Update changes list fields.

type Signup

type Signup struct {
	IP, UserAgent, Source, Consent string
	// From an import: when and from where the reader opted in on the old
	// platform, so consent proof and footers keep the real date.
	At, ConfirmedAt time.Time
	ConfirmIP       string
	// Reconfirm asks readers already on the list to confirm again, for a
	// stale import: they turn pending and get a confirmation mail.
	Reconfirm bool
}

Signup describes where a subscription came from: the consent proof.

type TextValue added in v0.2.2

type TextValue struct {
	Value      string `json:"value"`
	Overridden bool   `json:"overridden"`
}

TextValue includes the effective value and its source.

Jump to

Keyboard shortcuts

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