Refactor Messenger interface.

Remove `messenger.go` and move the interface definition to `manager`
and the `Message` struct to the `models` package, removing superflous
and redundant message structs used in multiple places.
This commit is contained in:
Kailash Nadh 2023-05-08 22:43:25 +05:30
parent 9ffc912a2c
commit 917696a543
11 changed files with 84 additions and 108 deletions

View file

@ -31,7 +31,6 @@ import (
"github.com/knadh/listmonk/internal/media"
"github.com/knadh/listmonk/internal/media/providers/filesystem"
"github.com/knadh/listmonk/internal/media/providers/s3"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/internal/messenger/email"
"github.com/knadh/listmonk/internal/messenger/postback"
"github.com/knadh/listmonk/internal/subimporter"
@ -484,7 +483,7 @@ func initImporter(q *models.Queries, db *sqlx.DB, app *App) *subimporter.Importe
}
// initSMTPMessenger initializes the SMTP messenger.
func initSMTPMessenger(m *manager.Manager) messenger.Messenger {
func initSMTPMessenger(m *manager.Manager) manager.Messenger {
var (
mapKeys = ko.MapKeys("smtp")
servers = make([]email.Server, 0, len(mapKeys))
@ -526,13 +525,13 @@ func initSMTPMessenger(m *manager.Manager) messenger.Messenger {
// initPostbackMessengers initializes and returns all the enabled
// HTTP postback messenger backends.
func initPostbackMessengers(m *manager.Manager) []messenger.Messenger {
func initPostbackMessengers(m *manager.Manager) []manager.Messenger {
items := ko.Slices("messengers")
if len(items) == 0 {
return nil
}
var out []messenger.Messenger
var out []manager.Messenger
for _, item := range items {
if !item.Bool("enabled") {
continue

View file

@ -22,7 +22,6 @@ import (
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/media"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/internal/subimporter"
"github.com/knadh/listmonk/models"
"github.com/knadh/paginator"
@ -43,7 +42,7 @@ type App struct {
constants *constants
manager *manager.Manager
importer *subimporter.Importer
messengers map[string]messenger.Messenger
messengers map[string]manager.Messenger
media media.Store
i18n *i18n.I18n
bounce *bounce.Manager
@ -167,7 +166,7 @@ func main() {
db: db,
constants: initConstants(),
media: initMediaStore(),
messengers: make(map[string]messenger.Messenger),
messengers: make(map[string]manager.Messenger),
log: lo,
bufLog: bufLog,
captcha: initCaptcha(),

View file

@ -3,7 +3,7 @@ package main
import (
"bytes"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/models"
)
const (
@ -32,7 +32,7 @@ func (app *App) sendNotification(toEmails []string, subject, tplName string, dat
return err
}
m := manager.Message{}
m := models.Message{}
m.ContentType = app.notifTpls.contentType
m.From = app.constants.FromEmail
m.To = toEmails

View file

@ -13,7 +13,7 @@ import (
"strings"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
@ -566,17 +566,17 @@ func handleSelfExportSubscriberData(c echo.Context) error {
// Send the data as a JSON attachment to the subscriber.
const fname = "data.json"
if err := app.messengers[emailMsgr].Push(messenger.Message{
if err := app.messengers[emailMsgr].Push(models.Message{
ContentType: app.notifTpls.contentType,
From: app.constants.FromEmail,
To: []string{data.Email},
Subject: app.i18n.Ts("email.data.title"),
Body: msg.Bytes(),
Attachments: []messenger.Attachment{
Attachments: []models.Attachment{
{
Name: fname,
Content: b,
Header: messenger.MakeAttachmentHeader(fname, "base64"),
Header: manager.MakeAttachmentHeader(fname, "base64"),
},
},
}); err != nil {

View file

@ -13,7 +13,6 @@ import (
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/v2"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/internal/messenger/email"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
@ -250,7 +249,7 @@ func handleTestSMTPSettings(c echo.Context) error {
return err
}
m := messenger.Message{}
m := models.Message{}
m.ContentType = app.notifTpls.contentType
m.From = app.constants.FromEmail
m.To = []string{to}

View file

@ -9,7 +9,6 @@ import (
"strings"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
)
@ -56,9 +55,9 @@ func handleSendTxMessage(c echo.Context) error {
app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error())))
}
m.Attachments = append(m.Attachments, models.TxAttachment{
m.Attachments = append(m.Attachments, models.Attachment{
Name: f.Filename,
Header: messenger.MakeAttachmentHeader(f.Filename, "base64"),
Header: manager.MakeAttachmentHeader(f.Filename, "base64"),
Content: b,
})
}
@ -121,7 +120,7 @@ func handleSendTxMessage(c echo.Context) error {
}
// Prepare the final message.
msg := manager.Message{}
msg := models.Message{}
msg.Subscriber = sub
msg.To = []string{sub.Email}
msg.From = m.FromEmail
@ -130,7 +129,7 @@ func handleSendTxMessage(c echo.Context) error {
msg.Messenger = m.Messenger
msg.Body = m.Body
for _, a := range m.Attachments {
msg.Attachments = append(msg.Attachments, messenger.Attachment{
msg.Attachments = append(msg.Attachments, models.Attachment{
Name: a.Name,
Header: a.Header,
Content: a.Content,

View file

@ -13,7 +13,6 @@ import (
"github.com/Masterminds/sprig/v3"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/models"
"github.com/paulbellamy/ratecounter"
)
@ -40,6 +39,15 @@ type Store interface {
DeleteSubscriber(id int64) error
}
// Messenger is an interface for a generic messaging backend,
// for instance, e-mail, SMS etc.
type Messenger interface {
Name() string
Push(models.Message) error
Flush() error
Close() error
}
// CampStats contains campaign stats like per minute send rate.
type CampStats struct {
SendRate int
@ -51,7 +59,7 @@ type Manager struct {
cfg Config
store Store
i18n *i18n.I18n
messengers map[string]messenger.Messenger
messengers map[string]Messenger
notifCB models.AdminNotifCallback
logger *log.Logger
@ -73,7 +81,7 @@ type Manager struct {
campMsgQueue chan CampaignMessage
campMsgErrorQueue chan msgError
campMsgErrorCounts map[int]int
msgQueue chan Message
msgQueue chan models.Message
// Sliding window keeps track of the total number of messages sent in a period
// and on reaching the specified limit, waits until the window is over before
@ -98,14 +106,6 @@ type CampaignMessage struct {
unsubURL string
}
// Message represents a generic message to be pushed to a messenger.
type Message struct {
messenger.Message
// Messenger is the messenger backend to use: email|postback.
Messenger string
}
// Config has parameters for configuring the manager.
type Config struct {
// Number of subscribers to pull from the DB in a single iteration.
@ -163,14 +163,14 @@ func New(cfg Config, store Store, notifCB models.AdminNotifCallback, i *i18n.I18
i18n: i,
notifCB: notifCB,
logger: l,
messengers: make(map[string]messenger.Messenger),
messengers: make(map[string]Messenger),
camps: make(map[int]*models.Campaign),
campRates: make(map[int]*ratecounter.RateCounter),
tpls: make(map[int]*models.Template),
links: make(map[string]string),
subFetchQueue: make(chan *models.Campaign, cfg.Concurrency),
campMsgQueue: make(chan CampaignMessage, cfg.Concurrency*2),
msgQueue: make(chan Message, cfg.Concurrency),
msgQueue: make(chan models.Message, cfg.Concurrency),
campMsgErrorQueue: make(chan msgError, cfg.MaxSendErrors),
campMsgErrorCounts: make(map[int]int),
slidingWindowStart: time.Now(),
@ -202,7 +202,7 @@ func (m *Manager) NewCampaignMessage(c *models.Campaign, s models.Subscriber) (C
}
// AddMessenger adds a Messenger messaging backend to the manager.
func (m *Manager) AddMessenger(msg messenger.Messenger) error {
func (m *Manager) AddMessenger(msg Messenger) error {
id := msg.Name()
if _, ok := m.messengers[id]; ok {
return fmt.Errorf("messenger '%s' is already loaded", id)
@ -213,7 +213,7 @@ func (m *Manager) AddMessenger(msg messenger.Messenger) error {
// PushMessage pushes an arbitrary non-campaign Message to be sent out by the workers.
// It times out if the queue is busy.
func (m *Manager) PushMessage(msg Message) error {
func (m *Manager) PushMessage(msg models.Message) error {
t := time.NewTicker(pushTimeout)
defer t.Stop()
@ -355,7 +355,7 @@ func (m *Manager) worker() {
numMsg++
// Outgoing message.
out := messenger.Message{
out := models.Message{
From: msg.from,
To: []string{msg.to},
Subject: msg.subject,
@ -410,7 +410,7 @@ func (m *Manager) worker() {
return
}
err := m.messengers[msg.Messenger].Push(msg.Message)
err := m.messengers[msg.Messenger].Push(msg)
if err != nil {
m.logger.Printf("error sending message '%s': %v", msg.Subject, err)
}
@ -801,3 +801,17 @@ func (m *Manager) makeGnericFuncMap() template.FuncMap {
return f
}
// MakeAttachmentHeader is a helper function that returns a
// textproto.MIMEHeader tailored for attachments, primarily
// email. If no encoding is given, base64 is assumed.
func MakeAttachmentHeader(filename, encoding string) textproto.MIMEHeader {
if encoding == "" {
encoding = "base64"
}
h := textproto.MIMEHeader{}
h.Set("Content-Disposition", "attachment; filename="+filename)
h.Set("Content-Type", "application/json; name=\""+filename+"\"")
h.Set("Content-Transfer-Encoding", encoding)
return h
}

View file

@ -7,7 +7,7 @@ import (
"net/smtp"
"net/textproto"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/models"
"github.com/knadh/smtppool"
)
@ -92,7 +92,7 @@ func (e *Emailer) Name() string {
}
// Push pushes a message to the server.
func (e *Emailer) Push(m messenger.Message) error {
func (e *Emailer) Push(m models.Message) error {
// If there are more than one SMTP servers, send to a random
// one from the list.
var (

View file

@ -1,55 +0,0 @@
package messenger
import (
"net/textproto"
"github.com/knadh/listmonk/models"
)
// Messenger is an interface for a generic messaging backend,
// for instance, e-mail, SMS etc.
type Messenger interface {
Name() string
Push(Message) error
Flush() error
Close() error
}
// Message is the message pushed to a Messenger.
type Message struct {
From string
To []string
Subject string
ContentType string
Body []byte
AltBody []byte
Headers textproto.MIMEHeader
Attachments []Attachment
Subscriber models.Subscriber
// Campaign is generally the same instance for a large number of subscribers.
Campaign *models.Campaign
}
// Attachment represents a file or blob attachment that can be
// sent along with a message by a Messenger.
type Attachment struct {
Name string
Header textproto.MIMEHeader
Content []byte
}
// MakeAttachmentHeader is a helper function that returns a
// textproto.MIMEHeader tailored for attachments, primarily
// email. If no encoding is given, base64 is assumed.
func MakeAttachmentHeader(filename, encoding string) textproto.MIMEHeader {
if encoding == "" {
encoding = "base64"
}
h := textproto.MIMEHeader{}
h.Set("Content-Disposition", "attachment; filename="+filename)
h.Set("Content-Type", "application/json; name=\""+filename+"\"")
h.Set("Content-Transfer-Encoding", encoding)
return h
}

View file

@ -10,11 +10,11 @@ import (
"net/textproto"
"time"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/models"
)
// postback is the payload that's posted as JSON to the HTTP Postback server.
//
//easyjson:json
type postback struct {
Subject string `json:"subject"`
@ -94,7 +94,7 @@ func (p *Postback) Name() string {
}
// Push pushes a message to the server.
func (p *Postback) Push(m messenger.Message) error {
func (p *Postback) Push(m models.Message) error {
pb := postback{
Subject: m.Subject,
ContentType: m.ContentType,

View file

@ -347,6 +347,34 @@ type Bounce struct {
Total int `db:"total" json:"-"`
}
// Message is the message pushed to a Messenger.
type Message struct {
From string
To []string
Subject string
ContentType string
Body []byte
AltBody []byte
Headers textproto.MIMEHeader
Attachments []Attachment
Subscriber Subscriber
// Campaign is generally the same instance for a large number of subscribers.
Campaign *Campaign
// Messenger is the messenger backend to use: email|postback.
Messenger string
}
// Attachment represents a file or blob attachment that can be
// sent along with a message by a Messenger.
type Attachment struct {
Name string
Header textproto.MIMEHeader
Content []byte
}
// TxMessage represents an e-mail campaign.
type TxMessage struct {
SubscriberEmails []string `json:"subscriber_emails"`
@ -364,7 +392,7 @@ type TxMessage struct {
Messenger string `json:"messenger"`
// File attachments added from multi-part form data.
Attachments []TxAttachment `json:"-"`
Attachments []Attachment `json:"-"`
Subject string `json:"-"`
Body []byte `json:"-"`
@ -372,13 +400,6 @@ type TxMessage struct {
SubjectTpl *txttpl.Template `json:"-"`
}
// TxAttachment is used by TxMessage, consists of FileName and file Content in bytes
type TxAttachment struct {
Name string
Header textproto.MIMEHeader
Content []byte
}
// markdown is a global instance of Markdown parser and renderer.
var markdown = goldmark.New(
goldmark.WithParserOptions(