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
- func Blocking(f []Finding) bool
- func CheckMedia(p string, data []byte) (string, error)
- func FrontMatter(src string) (map[string]string, string)
- func Rules() []finding.Rule
- func Slugify(s string) string
- type Draft
- type File
- type Finding
- type Issue
- type Links
- type Media
- type Recipient
- type Renderer
- func (r *Renderer) IssueURL(l *lists.List, w *Issue) string
- func (r *Renderer) Link(l *lists.List, w *Issue, i int) (string, bool)
- func (r *Renderer) MediaURL(l *lists.List, m Media) string
- func (r *Renderer) Message(l *lists.List, w *Issue, rc Recipient, test bool) ([]byte, error)
- func (r *Renderer) WebHTML(l *lists.List, w *Issue) (template.HTML, error)
- type Reply
- type Service
- func (s *Service) Archive(ctx context.Context, cutoff time.Time) (int, error)
- func (s *Service) Builder(r *Renderer) outbound.Builder
- func (s *Service) BySlug(ctx context.Context, listID int64, slug string) (*Issue, error)
- func (s *Service) Cancel(ctx context.Context, id int64) (*Issue, error)
- func (s *Service) Create(ctx context.Context, listSlug string, d Draft) (*Issue, error)
- func (s *Service) Crowding(ctx context.Context, id int64, at time.Time) (*Issue, error)
- func (s *Service) Get(ctx context.Context, id int64) (*Issue, error)
- func (s *Service) Lint(ctx context.Context, r *Renderer, id int64) ([]Finding, error)
- func (s *Service) List(ctx context.Context, listID int64, sentOnly bool, limit int) ([]*Issue, error)
- func (s *Service) Media(ctx context.Context, issueID int64) (map[string]Media, error)
- func (s *Service) MediaFile(ctx context.Context, listID int64, name string) ([]byte, string, error)
- func (s *Service) Missed(ctx context.Context) ([]*Issue, error)
- func (s *Service) Put(ctx context.Context, listSlug string, d Draft, welcome bool) (w *Issue, created bool, err error)
- func (s *Service) Remove(ctx context.Context, id int64) error
- func (s *Service) Schedule(ctx context.Context, id int64, at time.Time) (*Issue, error)
- func (s *Service) SetMedia(ctx context.Context, issueID int64, files []File) error
- func (s *Service) Stats(ctx context.Context, id int64) (Stats, error)
- func (s *Service) Test(ctx context.Context, id int64, to []string) error
- func (s *Service) Tick(ctx context.Context) error
- func (s *Service) Update(ctx context.Context, id int64, d Draft) (*Issue, error)
- func (s *Service) Welcome(ctx context.Context, l *lists.List, m *lists.Member) error
- type Stats
Constants ¶
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.
const MaxLate = 12 * time.Hour
Tick starts due issues and marks finished ones. The engine calls it every few seconds. 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 CheckMedia ¶
CheckMedia validates an image the way SetMedia will.
func FrontMatter ¶
FrontMatter splits optional "---" key: value lines from the body.
Types ¶
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 ¶
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.
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.
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 ¶
Archive freezes the numbers of issues finished before cutoff and deletes their per-recipient rows and events, which otherwise grow without bound.
func (*Service) Cancel ¶
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) Crowding ¶
Crowding returns another issue of the list that starts within the list's minimum gap of at, or nil.
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) Missed ¶
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) SetMedia ¶
SetMedia replaces an issue's images. A change resets its test copy, like a change of the text.
func (*Service) Test ¶
Test queues the issue to addresses outside the list, marked [Test], without tracking.
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.