issues

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: 28 Imported by: 0

Documentation

Overview

Package issues is the newsletter itself: issues written in Markdown, checked before sending, rendered per recipient with signed unsubscribe, click and open links, queued to every confirmed member, and counted afterwards.

Index

Constants

View Source
const (
	MaxMedia   = emailmd.MaxImage
	HeavyMedia = emailmd.HeavyImage
)

Images an issue references by a local path travel with it and are served from the list's website; emailmd.CheckImage holds the limits.

View Source
const MaxLate = 12 * time.Hour

MaxLate is how far past its slot a scheduled issue still starts by itself, after the server was down. Later, its text may no longer fit the day ("this weekend"), so it waits for the owner (Missed).

Variables

This section is empty.

Functions

func Blocking

func Blocking(f []Finding) bool

Blocking reports whether findings forbid sending.

func CheckMedia

func CheckMedia(p string, data []byte) (string, error)

CheckMedia validates an image the way SetMedia will.

func FrontMatter

func FrontMatter(src string) (map[string]string, string)

FrontMatter splits optional "---" key: value lines from the body.

func Rules

func Rules() []finding.Rule

Rules describes the checks only the server can run, on the rendered message and the list's settings; editorial.Rules has the rest.

func Slugify

func Slugify(s string) string

Slugify makes a URL path segment from a title.

Types

type Draft

type Draft struct {
	Markdown  string
	Subject   string
	Slug      string
	Preheader string
	Summary   string
}

Draft is the content of an issue; empty fields come from the Markdown.

type File

type File struct {
	Path string `json:"path"`
	Data []byte `json:"data"`
}

File is an image as the Markdown references it.

type Finding

type Finding = finding.Finding

Finding is one problem found before sending.

type Issue

type Issue struct {
	ID        int64     `json:"id"`
	ListID    int64     `json:"-"`
	List      string    `json:"list"`
	Slug      string    `json:"slug"`
	Subject   string    `json:"subject"`
	Preheader string    `json:"preheader,omitempty"`
	Summary   string    `json:"summary,omitempty"`
	Markdown  string    `json:"markdown,omitempty"`
	Status    string    `json:"status"`
	Scheduled time.Time `json:"scheduled,omitzero"`
	Started   time.Time `json:"started,omitzero"`
	Finished  time.Time `json:"finished,omitzero"`
	Tested    time.Time `json:"tested,omitzero"` // when this content last went out as a test copy
	Welcome   bool      `json:"welcome,omitempty"`
	Created   time.Time `json:"created"`
	Updated   time.Time `json:"updated"`
}

Issue is one issue of a list.

type Links struct {
	Unsub     string // https URL of the unsubscribe page (POST target for RFC 8058)
	UnsubMail string // mailto: address with token
	Pixel     string // open tracking image, empty without tracking
	Click     func(i int) string
	Web       string // web version
}

Links are the per-recipient URLs of one message.

type Media

type Media struct {
	Path string `json:"path"`
	Hash string `json:"hash"`
	Type string `json:"type"`
	Size int    `json:"size"`
}

Media is one stored image of an issue.

func (Media) Name

func (m Media) Name() string

Name is the file name the website serves the image under.

type Recipient

type Recipient struct {
	OutboundID int64
	MemberID   int64
	Email      string
	Name       string
	SignupAt   time.Time // zero for test copies
	Source     string    // where the signup came from ("web: https://…")
	Tracked    bool      // the reader consented to open and click measurement
}

Recipient is who a message is rendered for.

type Renderer

type Renderer struct {
	Tokens  *token.Signer
	ListURL func(l *lists.List) string // base URL of a list's site, no trailing slash
	BaseURL func(l *lists.List) string // where token links live (the list's site or the mail host)
	Layout  func(l *lists.List) *template.Template
	Media   func(issueID int64) map[string]Media // an issue's uploaded images; nil for none
	Now     func() time.Time
	// contains filtered or unexported fields
}

Renderer turns issues into messages. URLs come from the engine because they depend on the list's host and the server's host name.

func (*Renderer) IssueURL

func (r *Renderer) IssueURL(l *lists.List, w *Issue) string

IssueURL is the web version of an issue.

func (r *Renderer) Link(l *lists.List, w *Issue, i int) (string, bool)

Link returns the target of link i of an issue, for click redirects.

func (*Renderer) MediaURL

func (r *Renderer) MediaURL(l *lists.List, m Media) string

MediaURL is where the website serves an issue's image.

func (*Renderer) Message

func (r *Renderer) Message(l *lists.List, w *Issue, rc Recipient, test bool) ([]byte, error)

Message builds the complete unsigned message for one recipient.

func (*Renderer) WebHTML

func (r *Renderer) WebHTML(l *lists.List, w *Issue) (template.HTML, error)

WebHTML is the archive version of an issue: semantic HTML without tracking.

type Reply

type Reply struct {
	ID      int64  `json:"id"` // message ID for lfr mail read
	From    string `json:"from"`
	Snippet string `json:"snippet"`
}

Reply is one reader's answer to an issue, in the list's mailbox.

type Service

type Service struct {
	DB     *store.DB
	Lists  *lists.Service
	Events *events.Bus
	Queue  *outbound.Queue
	Blobs  *blob.Store // images of issues
	Now    func() time.Time
}

Service stores and sends issues.

func (*Service) Archive

func (s *Service) Archive(ctx context.Context, cutoff time.Time) (int, error)

Archive freezes the numbers of issues finished before cutoff and deletes their per-recipient rows and events, which otherwise grow without bound.

func (*Service) Builder

func (s *Service) Builder(r *Renderer) outbound.Builder

Builder returns the outbound builder for issue rows.

func (*Service) BySlug

func (s *Service) BySlug(ctx context.Context, listID int64, slug string) (*Issue, error)

BySlug loads an issue of a list.

func (*Service) Cancel

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

Cancel stops a scheduled or sending issue. A scheduled issue returns to draft; a sending issue keeps what went out and drops the rest.

func (*Service) Create

func (s *Service) Create(ctx context.Context, listSlug string, d Draft) (*Issue, error)

Create adds a draft issue to a list.

func (*Service) Crowding

func (s *Service) Crowding(ctx context.Context, id int64, at time.Time) (*Issue, error)

Crowding returns another issue of the list that starts within the list's minimum gap of at, or nil.

func (*Service) Get

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

Get loads an issue.

func (*Service) Lint

func (s *Service) Lint(ctx context.Context, r *Renderer, id int64) ([]Finding, error)

Lint checks an issue before it goes out.

func (*Service) List

func (s *Service) List(ctx context.Context, listID int64, sentOnly bool, limit int) ([]*Issue, error)

List returns the issues of a list, newest first; sentOnly limits to published ones.

func (*Service) Media

func (s *Service) Media(ctx context.Context, issueID int64) (map[string]Media, error)

Media returns an issue's images by path.

func (*Service) MediaFile

func (s *Service) MediaFile(ctx context.Context, listID int64, name string) ([]byte, string, error)

MediaFile returns an image of a list's issues by its served name.

func (*Service) Missed

func (s *Service) Missed(ctx context.Context) ([]*Issue, error)

Missed returns scheduled issues held because their slot passed more than MaxLate ago.

func (*Service) Put

func (s *Service) Put(ctx context.Context, listSlug string, d Draft, welcome bool) (w *Issue, created bool, err error)

Put creates or replaces the issue with d's slug on a list, so a file on disk and its issue on the server stay one thing. An issue that went out cannot change. welcome makes it the list's welcome issue; there is one.

func (*Service) Remove

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

Remove deletes an issue that is not being sent.

func (*Service) Schedule

func (s *Service) Schedule(ctx context.Context, id int64, at time.Time) (*Issue, error)

Schedule makes a draft go out at at (now or earlier: immediately).

func (*Service) SetMedia

func (s *Service) SetMedia(ctx context.Context, issueID int64, files []File) error

SetMedia replaces an issue's images. A change resets its test copy, like a change of the text.

func (*Service) Stats

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

Stats reports an issue's numbers.

func (*Service) Test

func (s *Service) Test(ctx context.Context, id int64, to []string) error

Test queues the issue to addresses outside the list, marked [Test], without tracking.

func (*Service) Tick

func (s *Service) Tick(ctx context.Context) error

Tick starts due issues and marks finished ones. The engine calls it every few seconds.

func (*Service) Update

func (s *Service) Update(ctx context.Context, id int64, d Draft) (*Issue, error)

Update replaces the content of a draft or scheduled issue.

func (*Service) Welcome

func (s *Service) Welcome(ctx context.Context, l *lists.List, m *lists.Member) error

Welcome queues the list's welcome issue to a member who just confirmed. Each address gets it once.

type Stats

type Stats struct {
	Recipients   int            `json:"recipients"`
	Queued       int            `json:"queued"`
	Sent         int            `json:"sent"`
	Bounced      int            `json:"bounced"`
	Failed       int            `json:"failed"`
	Canceled     int            `json:"canceled"`
	Opened       int            `json:"opened"`  // unique recipients, includes machine opens
	Clicked      int            `json:"clicked"` // unique recipients
	Unsubscribed int            `json:"unsubscribed"`
	Complained   int            `json:"complained"`
	Replies      int            `json:"replies"`               // distinct senders who answered in the list's mailbox
	Links        map[string]int `json:"link_clicks,omitempty"` // unique clicks per URL
	Tracking     bool           `json:"tracking"`              // opens and clicks were measured
	ReplyList    []Reply        `json:"reply_list,omitempty"`  // the replies themselves, newest first
}

Stats counts what happened to an issue.

Jump to

Keyboard shortcuts

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