Add generic HTTP postback `Messenger` support.
This is a major feature that builds upon the `Messenger` interface that has been in listmonk since its inception (with SMTP as the only messenger). This commit introduces a new Messenger implementation, an HTTP "postback", that can post campaign messages as a standard JSON payload to arbitrary HTTP servers. These servers can in turn push them to FCM, SMS, or any or any such upstream, enabling listmonk to be a generic campaign messenger for any type of communication, not just e-mails. Postback HTTP endpoints can be defined in settings and they can be selected on campaigns.
This commit is contained in:
parent
be9fbcd542
commit
6cf43ea674
14
cmd/admin.go
14
cmd/admin.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -29,11 +30,22 @@ func handleGetConfigScript(c echo.Context) error {
|
||||||
out = configScript{
|
out = configScript{
|
||||||
RootURL: app.constants.RootURL,
|
RootURL: app.constants.RootURL,
|
||||||
FromEmail: app.constants.FromEmail,
|
FromEmail: app.constants.FromEmail,
|
||||||
Messengers: app.manager.GetMessengerNames(),
|
|
||||||
MediaProvider: app.constants.MediaProvider,
|
MediaProvider: app.constants.MediaProvider,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sort messenger names with `email` always as the first item.
|
||||||
|
var names []string
|
||||||
|
for name := range app.messengers {
|
||||||
|
if name == emailMsgr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
out.Messengers = append(out.Messengers, emailMsgr)
|
||||||
|
out.Messengers = append(out.Messengers, names...)
|
||||||
|
|
||||||
app.Lock()
|
app.Lock()
|
||||||
out.NeedsRestart = app.needsRestart
|
out.NeedsRestart = app.needsRestart
|
||||||
out.Update = app.update
|
out.Update = app.update
|
||||||
|
|
|
@ -220,11 +220,6 @@ func handleCreateCampaign(c echo.Context) error {
|
||||||
o = c
|
o = c
|
||||||
}
|
}
|
||||||
|
|
||||||
if !app.manager.HasMessenger(o.MessengerID) {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Unknown messenger %s", o.MessengerID))
|
|
||||||
}
|
|
||||||
|
|
||||||
uu, err := uuid.NewV4()
|
uu, err := uuid.NewV4()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.log.Printf("error generating UUID: %v", err)
|
app.log.Printf("error generating UUID: %v", err)
|
||||||
|
@ -243,7 +238,7 @@ func handleCreateCampaign(c echo.Context) error {
|
||||||
o.ContentType,
|
o.ContentType,
|
||||||
o.SendAt,
|
o.SendAt,
|
||||||
pq.StringArray(normalizeTags(o.Tags)),
|
pq.StringArray(normalizeTags(o.Tags)),
|
||||||
"email",
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
o.ListIDs,
|
o.ListIDs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -312,6 +307,7 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
o.SendAt,
|
o.SendAt,
|
||||||
o.SendLater,
|
o.SendLater,
|
||||||
pq.StringArray(normalizeTags(o.Tags)),
|
pq.StringArray(normalizeTags(o.Tags)),
|
||||||
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
o.ListIDs)
|
o.ListIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -492,6 +488,7 @@ func handleTestCampaign(c echo.Context) error {
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate.
|
// Validate.
|
||||||
if c, err := validateCampaignFields(req, app); err != nil {
|
if c, err := validateCampaignFields(req, app); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
@ -532,6 +529,9 @@ func handleTestCampaign(c echo.Context) error {
|
||||||
camp.Subject = req.Subject
|
camp.Subject = req.Subject
|
||||||
camp.FromEmail = req.FromEmail
|
camp.FromEmail = req.FromEmail
|
||||||
camp.Body = req.Body
|
camp.Body = req.Body
|
||||||
|
camp.Messenger = req.Messenger
|
||||||
|
camp.ContentType = req.ContentType
|
||||||
|
camp.TemplateID = req.TemplateID
|
||||||
|
|
||||||
// Send the test messages.
|
// Send the test messages.
|
||||||
for _, s := range subs {
|
for _, s := range subs {
|
||||||
|
@ -560,11 +560,14 @@ func sendTestMessage(sub models.Subscriber, camp *models.Campaign, app *App) err
|
||||||
fmt.Sprintf("Error rendering message: %v", err))
|
fmt.Sprintf("Error rendering message: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.messenger.Push(messenger.Message{
|
return app.messengers[camp.Messenger].Push(messenger.Message{
|
||||||
From: camp.FromEmail,
|
From: camp.FromEmail,
|
||||||
To: []string{sub.Email},
|
To: []string{sub.Email},
|
||||||
Subject: m.Subject(),
|
Subject: m.Subject(),
|
||||||
|
ContentType: camp.ContentType,
|
||||||
Body: m.Body(),
|
Body: m.Body(),
|
||||||
|
Subscriber: sub,
|
||||||
|
Campaign: camp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,9 +603,13 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
|
||||||
return c, errors.New("no lists selected")
|
return c, errors.New("no lists selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !app.manager.HasMessenger(c.Messenger) {
|
||||||
|
return c, fmt.Errorf("unknown messenger %s", c.Messenger)
|
||||||
|
}
|
||||||
|
|
||||||
camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
|
camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
|
||||||
if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||||
return c, fmt.Errorf("Error compiling campaign body: %v", err)
|
return c, fmt.Errorf("error compiling campaign body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
|
|
60
cmd/init.go
60
cmd/init.go
|
@ -25,6 +25,8 @@ import (
|
||||||
"github.com/knadh/listmonk/internal/media/providers/filesystem"
|
"github.com/knadh/listmonk/internal/media/providers/filesystem"
|
||||||
"github.com/knadh/listmonk/internal/media/providers/s3"
|
"github.com/knadh/listmonk/internal/media/providers/s3"
|
||||||
"github.com/knadh/listmonk/internal/messenger"
|
"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"
|
"github.com/knadh/listmonk/internal/subimporter"
|
||||||
"github.com/knadh/stuffbin"
|
"github.com/knadh/stuffbin"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
|
@ -290,11 +292,11 @@ func initImporter(q *Queries, db *sqlx.DB, app *App) *subimporter.Importer {
|
||||||
}, db.DB)
|
}, db.DB)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initMessengers initializes various messenger backends.
|
// initSMTPMessenger initializes the SMTP messenger.
|
||||||
func initMessengers(m *manager.Manager) messenger.Messenger {
|
func initSMTPMessenger(m *manager.Manager) messenger.Messenger {
|
||||||
var (
|
var (
|
||||||
mapKeys = ko.MapKeys("smtp")
|
mapKeys = ko.MapKeys("smtp")
|
||||||
servers = make([]messenger.Server, 0, len(mapKeys))
|
servers = make([]email.Server, 0, len(mapKeys))
|
||||||
)
|
)
|
||||||
|
|
||||||
items := ko.Slices("smtp")
|
items := ko.Slices("smtp")
|
||||||
|
@ -302,37 +304,71 @@ func initMessengers(m *manager.Manager) messenger.Messenger {
|
||||||
lo.Fatalf("no SMTP servers found in config")
|
lo.Fatalf("no SMTP servers found in config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the default SMTP messengers.
|
// Load the config for multipme SMTP servers.
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if !item.Bool("enabled") {
|
if !item.Bool("enabled") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the SMTP config.
|
// Read the SMTP config.
|
||||||
var s messenger.Server
|
var s email.Server
|
||||||
if err := item.UnmarshalWithConf("", &s, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
if err := item.UnmarshalWithConf("", &s, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||||
lo.Fatalf("error loading SMTP: %v", err)
|
lo.Fatalf("error reading SMTP config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
servers = append(servers, s)
|
servers = append(servers, s)
|
||||||
lo.Printf("loaded SMTP: %s@%s", item.String("username"), item.String("host"))
|
lo.Printf("loaded email (SMTP) messenger: %s@%s",
|
||||||
|
item.String("username"), item.String("host"))
|
||||||
}
|
}
|
||||||
if len(servers) == 0 {
|
if len(servers) == 0 {
|
||||||
lo.Fatalf("no SMTP servers enabled in settings")
|
lo.Fatalf("no SMTP servers enabled in settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the default e-mail messenger.
|
// Initialize the e-mail messenger with multiple SMTP servers.
|
||||||
msgr, err := messenger.NewEmailer(servers...)
|
msgr, err := email.New(servers...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lo.Fatalf("error loading e-mail messenger: %v", err)
|
lo.Fatalf("error loading e-mail messenger: %v", err)
|
||||||
}
|
}
|
||||||
if err := m.AddMessenger(msgr); err != nil {
|
|
||||||
lo.Printf("error registering messenger %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return msgr
|
return msgr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initPostbackMessengers initializes and returns all the enabled
|
||||||
|
// HTTP postback messenger backends.
|
||||||
|
func initPostbackMessengers(m *manager.Manager) []messenger.Messenger {
|
||||||
|
items := ko.Slices("messengers")
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []messenger.Messenger
|
||||||
|
for _, item := range items {
|
||||||
|
if !item.Bool("enabled") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the Postback server config.
|
||||||
|
var (
|
||||||
|
name = item.String("name")
|
||||||
|
o postback.Options
|
||||||
|
)
|
||||||
|
if err := item.UnmarshalWithConf("", &o, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||||
|
lo.Fatalf("error reading Postback config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the Messenger.
|
||||||
|
p, err := postback.New(o)
|
||||||
|
if err != nil {
|
||||||
|
lo.Fatalf("error initializing Postback messenger %s: %v", name, err)
|
||||||
|
}
|
||||||
|
out = append(out, p)
|
||||||
|
|
||||||
|
lo.Printf("loaded Postback messenger: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// initMediaStore initializes Upload manager with a custom backend.
|
// initMediaStore initializes Upload manager with a custom backend.
|
||||||
func initMediaStore() media.Store {
|
func initMediaStore() media.Store {
|
||||||
switch provider := ko.String("upload.provider"); provider {
|
switch provider := ko.String("upload.provider"); provider {
|
||||||
|
|
|
@ -123,7 +123,7 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
|
||||||
"richtext",
|
"richtext",
|
||||||
nil,
|
nil,
|
||||||
pq.StringArray{"test-campaign"},
|
pq.StringArray{"test-campaign"},
|
||||||
"email",
|
emailMsgr,
|
||||||
1,
|
1,
|
||||||
pq.Int64Array{1},
|
pq.Int64Array{1},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
25
cmd/main.go
25
cmd/main.go
|
@ -22,6 +22,10 @@ import (
|
||||||
"github.com/knadh/stuffbin"
|
"github.com/knadh/stuffbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emailMsgr = "email"
|
||||||
|
)
|
||||||
|
|
||||||
// App contains the "global" components that are
|
// App contains the "global" components that are
|
||||||
// passed around, especially through HTTP handlers.
|
// passed around, especially through HTTP handlers.
|
||||||
type App struct {
|
type App struct {
|
||||||
|
@ -31,7 +35,7 @@ type App struct {
|
||||||
constants *constants
|
constants *constants
|
||||||
manager *manager.Manager
|
manager *manager.Manager
|
||||||
importer *subimporter.Importer
|
importer *subimporter.Importer
|
||||||
messenger messenger.Messenger
|
messengers map[string]messenger.Messenger
|
||||||
media media.Store
|
media media.Store
|
||||||
notifTpls *template.Template
|
notifTpls *template.Template
|
||||||
log *log.Logger
|
log *log.Logger
|
||||||
|
@ -126,14 +130,27 @@ func main() {
|
||||||
db: db,
|
db: db,
|
||||||
constants: initConstants(),
|
constants: initConstants(),
|
||||||
media: initMediaStore(),
|
media: initMediaStore(),
|
||||||
|
messengers: make(map[string]messenger.Messenger),
|
||||||
log: lo,
|
log: lo,
|
||||||
}
|
}
|
||||||
_, app.queries = initQueries(queryFilePath, db, fs, true)
|
_, app.queries = initQueries(queryFilePath, db, fs, true)
|
||||||
app.manager = initCampaignManager(app.queries, app.constants, app)
|
app.manager = initCampaignManager(app.queries, app.constants, app)
|
||||||
app.importer = initImporter(app.queries, db, app)
|
app.importer = initImporter(app.queries, db, app)
|
||||||
app.messenger = initMessengers(app.manager)
|
|
||||||
app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.constants)
|
app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.constants)
|
||||||
|
|
||||||
|
// Initialize the default SMTP (`email`) messenger.
|
||||||
|
app.messengers[emailMsgr] = initSMTPMessenger(app.manager)
|
||||||
|
|
||||||
|
// Initialize any additional postback messengers.
|
||||||
|
for _, m := range initPostbackMessengers(app.manager) {
|
||||||
|
app.messengers[m.Name()] = m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach all messengers to the campaign manager.
|
||||||
|
for _, m := range app.messengers {
|
||||||
|
app.manager.AddMessenger(m)
|
||||||
|
}
|
||||||
|
|
||||||
// Start the campaign workers. The campaign batches (fetch from DB, push out
|
// Start the campaign workers. The campaign batches (fetch from DB, push out
|
||||||
// messages) get processed at the specified interval.
|
// messages) get processed at the specified interval.
|
||||||
go app.manager.Run(time.Second * 5)
|
go app.manager.Run(time.Second * 5)
|
||||||
|
@ -164,7 +181,9 @@ func main() {
|
||||||
app.db.DB.Close()
|
app.db.DB.Close()
|
||||||
|
|
||||||
// Close the messenger pool.
|
// Close the messenger pool.
|
||||||
app.messenger.Close()
|
for _, m := range app.messengers {
|
||||||
|
m.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// Signal the close.
|
// Signal the close.
|
||||||
closerWait <- true
|
closerWait <- true
|
||||||
|
|
|
@ -28,14 +28,13 @@ func (app *App) sendNotification(toEmails []string, subject, tplName string, dat
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.manager.PushMessage(manager.Message{
|
m := manager.Message{}
|
||||||
From: app.constants.FromEmail,
|
m.From = app.constants.FromEmail
|
||||||
To: toEmails,
|
m.To = toEmails
|
||||||
Subject: subject,
|
m.Subject = subject
|
||||||
Body: b.Bytes(),
|
m.Body = b.Bytes()
|
||||||
Messenger: "email",
|
m.Messenger = emailMsgr
|
||||||
})
|
if err := app.manager.PushMessage(m); err != nil {
|
||||||
if err != nil {
|
|
||||||
app.log.Printf("error sending admin notification (%s): %v", subject, err)
|
app.log.Printf("error sending admin notification (%s): %v", subject, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -367,7 +367,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
|
||||||
|
|
||||||
// Send the data as a JSON attachment to the subscriber.
|
// Send the data as a JSON attachment to the subscriber.
|
||||||
const fname = "data.json"
|
const fname = "data.json"
|
||||||
if err := app.messenger.Push(messenger.Message{
|
if err := app.messengers[emailMsgr].Push(messenger.Message{
|
||||||
From: app.constants.FromEmail,
|
From: app.constants.FromEmail,
|
||||||
To: []string{data.Email},
|
To: []string{data.Email},
|
||||||
Subject: "Your data",
|
Subject: "Your data",
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -22,14 +24,24 @@ type settings struct {
|
||||||
AppMaxSendErrors int `json:"app.max_send_errors"`
|
AppMaxSendErrors int `json:"app.max_send_errors"`
|
||||||
AppMessageRate int `json:"app.message_rate"`
|
AppMessageRate int `json:"app.message_rate"`
|
||||||
|
|
||||||
Messengers []interface{} `json:"messengers"`
|
|
||||||
|
|
||||||
PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
|
PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
|
||||||
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
|
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
|
||||||
PrivacyAllowExport bool `json:"privacy.allow_export"`
|
PrivacyAllowExport bool `json:"privacy.allow_export"`
|
||||||
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
|
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
|
||||||
PrivacyExportable []string `json:"privacy.exportable"`
|
PrivacyExportable []string `json:"privacy.exportable"`
|
||||||
|
|
||||||
|
UploadProvider string `json:"upload.provider"`
|
||||||
|
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
|
||||||
|
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
|
||||||
|
UploadS3AwsAccessKeyID string `json:"upload.s3.aws_access_key_id"`
|
||||||
|
UploadS3AwsDefaultRegion string `json:"upload.s3.aws_default_region"`
|
||||||
|
UploadS3AwsSecretAccessKey string `json:"upload.s3.aws_secret_access_key,omitempty"`
|
||||||
|
UploadS3Bucket string `json:"upload.s3.bucket"`
|
||||||
|
UploadS3BucketDomain string `json:"upload.s3.bucket_domain"`
|
||||||
|
UploadS3BucketPath string `json:"upload.s3.bucket_path"`
|
||||||
|
UploadS3BucketType string `json:"upload.s3.bucket_type"`
|
||||||
|
UploadS3Expiry string `json:"upload.s3.expiry"`
|
||||||
|
|
||||||
SMTP []struct {
|
SMTP []struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
@ -47,21 +59,22 @@ type settings struct {
|
||||||
TLSSkipVerify bool `json:"tls_skip_verify"`
|
TLSSkipVerify bool `json:"tls_skip_verify"`
|
||||||
} `json:"smtp"`
|
} `json:"smtp"`
|
||||||
|
|
||||||
UploadProvider string `json:"upload.provider"`
|
Messengers []struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
|
Name string `json:"name"`
|
||||||
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
|
RootURL string `json:"root_url"`
|
||||||
|
Username string `json:"username"`
|
||||||
UploadS3AwsAccessKeyID string `json:"upload.s3.aws_access_key_id"`
|
Password string `json:"password,omitempty"`
|
||||||
UploadS3AwsDefaultRegion string `json:"upload.s3.aws_default_region"`
|
MaxConns int `json:"max_conns"`
|
||||||
UploadS3AwsSecretAccessKey string `json:"upload.s3.aws_secret_access_key,omitempty"`
|
Timeout string `json:"timeout"`
|
||||||
UploadS3Bucket string `json:"upload.s3.bucket"`
|
MaxMsgRetries int `json:"max_msg_retries"`
|
||||||
UploadS3BucketDomain string `json:"upload.s3.bucket_domain"`
|
} `json:"messengers"`
|
||||||
UploadS3BucketPath string `json:"upload.s3.bucket_path"`
|
|
||||||
UploadS3BucketType string `json:"upload.s3.bucket_type"`
|
|
||||||
UploadS3Expiry string `json:"upload.s3.expiry"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
|
||||||
|
)
|
||||||
|
|
||||||
// handleGetSettings returns settings from the DB.
|
// handleGetSettings returns settings from the DB.
|
||||||
func handleGetSettings(c echo.Context) error {
|
func handleGetSettings(c echo.Context) error {
|
||||||
app := c.Get("app").(*App)
|
app := c.Get("app").(*App)
|
||||||
|
@ -75,6 +88,9 @@ func handleGetSettings(c echo.Context) error {
|
||||||
for i := 0; i < len(s.SMTP); i++ {
|
for i := 0; i < len(s.SMTP); i++ {
|
||||||
s.SMTP[i].Password = ""
|
s.SMTP[i].Password = ""
|
||||||
}
|
}
|
||||||
|
for i := 0; i < len(s.Messengers); i++ {
|
||||||
|
s.Messengers[i].Password = ""
|
||||||
|
}
|
||||||
s.UploadS3AwsSecretAccessKey = ""
|
s.UploadS3AwsSecretAccessKey = ""
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, okResp{s})
|
return c.JSON(http.StatusOK, okResp{s})
|
||||||
|
@ -111,13 +127,43 @@ func handleUpdateSettings(c echo.Context) error {
|
||||||
if len(cur.SMTP) > i &&
|
if len(cur.SMTP) > i &&
|
||||||
set.SMTP[i].Host == cur.SMTP[i].Host &&
|
set.SMTP[i].Host == cur.SMTP[i].Host &&
|
||||||
set.SMTP[i].Username == cur.SMTP[i].Username {
|
set.SMTP[i].Username == cur.SMTP[i].Username {
|
||||||
|
// Copy the existing password as password's needn't be
|
||||||
|
// sent from the frontend for updating entries.
|
||||||
set.SMTP[i].Password = cur.SMTP[i].Password
|
set.SMTP[i].Password = cur.SMTP[i].Password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !has {
|
if !has {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
"Minimum one SMTP block should be enabled.")
|
"At least one SMTP block should be enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize postback Messenger names. Duplicates are disallowed
|
||||||
|
// and "email" is a reserved name.
|
||||||
|
names := map[string]bool{emailMsgr: true}
|
||||||
|
|
||||||
|
for i := range set.Messengers {
|
||||||
|
if set.Messengers[i].Password == "" {
|
||||||
|
if len(cur.Messengers) > i &&
|
||||||
|
set.Messengers[i].RootURL == cur.Messengers[i].RootURL &&
|
||||||
|
set.Messengers[i].Username == cur.Messengers[i].Username {
|
||||||
|
// Copy the existing password as password's needn't be
|
||||||
|
// sent from the frontend for updating entries.
|
||||||
|
set.Messengers[i].Password = cur.Messengers[i].Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name := reAlphaNum.ReplaceAllString(strings.ToLower(set.Messengers[i].Name), "")
|
||||||
|
if _, ok := names[name]; ok {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Duplicate messenger name `%s`.", name))
|
||||||
|
}
|
||||||
|
if len(name) == 0 {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid messenger name.")
|
||||||
|
}
|
||||||
|
|
||||||
|
set.Messengers[i].Name = name
|
||||||
|
names[name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3 password?
|
// S3 password?
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
|
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
<script src="<%= BASE_URL %>api/config.js"></script>
|
<script src="<%= BASE_URL %>api/config.js" id="server-config"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|
|
@ -11,11 +11,14 @@
|
||||||
<b-radio v-model="form.radioFormat"
|
<b-radio v-model="form.radioFormat"
|
||||||
@input="onChangeFormat" :disabled="disabled" name="format"
|
@input="onChangeFormat" :disabled="disabled" name="format"
|
||||||
native-value="html">Raw HTML</b-radio>
|
native-value="html">Raw HTML</b-radio>
|
||||||
|
<b-radio v-model="form.radioFormat"
|
||||||
|
@input="onChangeFormat" :disabled="disabled" name="format"
|
||||||
|
native-value="plain">Plain text</b-radio>
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6 has-text-right">
|
<div class="column is-6 has-text-right">
|
||||||
<b-button @click="togglePreview" type="is-primary"
|
<b-button @click="onTogglePreview" type="is-primary"
|
||||||
icon-left="file-find-outline">Preview</b-button>
|
icon-left="file-find-outline">Preview</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,9 +40,13 @@
|
||||||
<div v-if="form.format === 'html'"
|
<div v-if="form.format === 'html'"
|
||||||
ref="htmlEditor" id="html-editor" class="html-editor"></div>
|
ref="htmlEditor" id="html-editor" class="html-editor"></div>
|
||||||
|
|
||||||
|
<!-- plain text editor //-->
|
||||||
|
<b-input v-if="form.format === 'plain'" v-model="form.body" @input="onEditorChange"
|
||||||
|
type="textarea" ref="plainEditor" class="plain-editor" />
|
||||||
|
|
||||||
<!-- campaign preview //-->
|
<!-- campaign preview //-->
|
||||||
<campaign-preview v-if="isPreviewing"
|
<campaign-preview v-if="isPreviewing"
|
||||||
@close="togglePreview"
|
@close="onTogglePreview"
|
||||||
type='campaign'
|
type='campaign'
|
||||||
:id='id'
|
:id='id'
|
||||||
:title='title'
|
:title='title'
|
||||||
|
@ -136,7 +143,7 @@ export default {
|
||||||
esc: {
|
esc: {
|
||||||
key: 27,
|
key: 27,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.toggleFullscreen(true);
|
this.onToggleFullscreen(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -163,8 +170,8 @@ export default {
|
||||||
],
|
],
|
||||||
|
|
||||||
handlers: {
|
handlers: {
|
||||||
image: this.toggleMedia,
|
image: this.onToggleMedia,
|
||||||
fullscreen: () => this.toggleFullscreen(false),
|
fullscreen: () => this.onToggleFullscreen(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -227,16 +234,16 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePreview() {
|
onTogglePreview() {
|
||||||
this.isPreviewing = !this.isPreviewing;
|
this.isPreviewing = !this.isPreviewing;
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleMedia() {
|
onToggleMedia() {
|
||||||
this.lastSel = this.$refs.quill.quill.getSelection();
|
this.lastSel = this.$refs.quill.quill.getSelection();
|
||||||
this.isMediaVisible = !this.isMediaVisible;
|
this.isMediaVisible = !this.isMediaVisible;
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleFullscreen(onlyMinimize) {
|
onToggleFullscreen(onlyMinimize) {
|
||||||
if (onlyMinimize) {
|
if (onlyMinimize) {
|
||||||
if (!this.isEditorFullscreen) {
|
if (!this.isEditorFullscreen) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -16,6 +16,22 @@ Vue.config.productionTip = false;
|
||||||
Vue.prototype.$api = api;
|
Vue.prototype.$api = api;
|
||||||
Vue.prototype.$utils = utils;
|
Vue.prototype.$utils = utils;
|
||||||
|
|
||||||
|
Vue.prototype.$reloadServerConfig = () => {
|
||||||
|
// Get the config.js <script> tag, remove it, and re-add it.
|
||||||
|
let s = document.querySelector('#server-config');
|
||||||
|
const url = s.getAttribute('src');
|
||||||
|
s.remove();
|
||||||
|
|
||||||
|
s = document.createElement('script');
|
||||||
|
s.setAttribute('src', url);
|
||||||
|
s.setAttribute('id', 'server-config');
|
||||||
|
s.onload = () => {
|
||||||
|
store.commit('setModelResponse',
|
||||||
|
{ model: models.serverConfig, data: humps.camelizeKeys(window.CONFIG) });
|
||||||
|
};
|
||||||
|
document.body.appendChild(s);
|
||||||
|
};
|
||||||
|
|
||||||
// window.CONFIG is loaded from /api/config.js directly in a <script> tag.
|
// window.CONFIG is loaded from /api/config.js directly in a <script> tag.
|
||||||
if (window.CONFIG) {
|
if (window.CONFIG) {
|
||||||
store.commit('setModelResponse',
|
store.commit('setModelResponse',
|
||||||
|
|
|
@ -69,6 +69,14 @@
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Messenger" label-position="on-border">
|
||||||
|
<b-select placeholder="Messenger" v-model="form.messenger"
|
||||||
|
:disabled="!canEdit" required>
|
||||||
|
<option v-for="m in serverConfig.messengers"
|
||||||
|
:value="m" :key="m">{{ m }}</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Tags" label-position="on-border">
|
<b-field label="Tags" label-position="on-border">
|
||||||
<b-taginput v-model="form.tags" :disabled="!canEdit"
|
<b-taginput v-model="form.tags" :disabled="!canEdit"
|
||||||
ellipsis icon="tag-outline" placeholder="Tags"></b-taginput>
|
ellipsis icon="tag-outline" placeholder="Tags"></b-taginput>
|
||||||
|
@ -206,12 +214,12 @@ export default Vue.extend({
|
||||||
subject: this.form.subject,
|
subject: this.form.subject,
|
||||||
lists: this.form.lists.map((l) => l.id),
|
lists: this.form.lists.map((l) => l.id),
|
||||||
from_email: this.form.fromEmail,
|
from_email: this.form.fromEmail,
|
||||||
content_type: 'richtext',
|
messenger: this.form.messenger,
|
||||||
messenger: 'email',
|
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
tags: this.form.tags,
|
tags: this.form.tags,
|
||||||
template_id: this.form.templateId,
|
template_id: this.form.templateId,
|
||||||
body: this.form.body,
|
content_type: this.form.content.contentType,
|
||||||
|
body: this.form.content.body,
|
||||||
subscribers: this.form.testEmails,
|
subscribers: this.form.testEmails,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -255,7 +263,7 @@ export default Vue.extend({
|
||||||
subject: this.form.subject,
|
subject: this.form.subject,
|
||||||
lists: this.form.lists.map((l) => l.id),
|
lists: this.form.lists.map((l) => l.id),
|
||||||
from_email: this.form.fromEmail,
|
from_email: this.form.fromEmail,
|
||||||
messenger: 'email',
|
messenger: this.form.messenger,
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
tags: this.form.tags,
|
tags: this.form.tags,
|
||||||
send_later: this.form.sendLater,
|
send_later: this.form.sendLater,
|
||||||
|
@ -305,7 +313,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['lists', 'templates', 'loading']),
|
...mapState(['serverConfig', 'loading', 'lists', 'templates']),
|
||||||
|
|
||||||
canEdit() {
|
canEdit() {
|
||||||
return this.isNew
|
return this.isNew
|
||||||
|
@ -353,6 +361,8 @@ export default Vue.extend({
|
||||||
this.activeTab = 1;
|
this.activeTab = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.form.messenger = 'email';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
|
@ -183,7 +183,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['lists', 'serverConfig', 'loading']),
|
...mapState(['serverConfig', 'loading', 'lists']),
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -330,8 +330,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<b-field label="Retries" label-position="on-border"
|
<b-field label="Retries" label-position="on-border"
|
||||||
message="The number of times a message should be retried
|
message="Number of times to rety when a message fails.">
|
||||||
if sending fails.">
|
|
||||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
||||||
type="is-light"
|
type="is-light"
|
||||||
controls-position="compact"
|
controls-position="compact"
|
||||||
|
@ -378,7 +377,95 @@
|
||||||
|
|
||||||
<b-button @click="addSMTP" icon-left="plus" type="is-primary">Add new</b-button>
|
<b-button @click="addSMTP" icon-left="plus" type="is-primary">Add new</b-button>
|
||||||
</b-tab-item><!-- mail servers -->
|
</b-tab-item><!-- mail servers -->
|
||||||
|
|
||||||
|
<b-tab-item label="Messengers">
|
||||||
|
<div class="items messengers">
|
||||||
|
<div class="block box" v-for="(item, n) in form.messengers" :key="n">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-2">
|
||||||
|
<b-field label="Enabled">
|
||||||
|
<b-switch v-model="item.enabled" name="enabled"
|
||||||
|
:native-value="true" />
|
||||||
|
</b-field>
|
||||||
|
<b-field>
|
||||||
|
<a @click.prevent="$utils.confirm(null, () => removeMessenger(n))"
|
||||||
|
href="#" class="is-size-7">
|
||||||
|
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||||
|
</a>
|
||||||
|
</b-field>
|
||||||
|
</div><!-- first column -->
|
||||||
|
|
||||||
|
<div class="column" :class="{'disabled': !item.enabled}">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-4">
|
||||||
|
<b-field label="Name" label-position="on-border"
|
||||||
|
message="eg: my-sms. Alphanumeric / dash.">
|
||||||
|
<b-input v-model="item.name" name="name"
|
||||||
|
placeholder='mymessenger' :maxlength="200" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div class="column is-8">
|
||||||
|
<b-field label="URL" label-position="on-border"
|
||||||
|
message="Root URL of the Postback server.">
|
||||||
|
<b-input v-model="item.root_url" name="root_url"
|
||||||
|
placeholder='https://postback.messenger.net/path' :maxlength="200" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</div><!-- host -->
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<b-field grouped>
|
||||||
|
<b-field label="Username" label-position="on-border" expanded>
|
||||||
|
<b-input v-model="item.username" name="username" :maxlength="200" />
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Password" label-position="on-border" expanded
|
||||||
|
message="Enter a value to change.">
|
||||||
|
<b-input v-model="item.password"
|
||||||
|
name="password" type="password" placeholder="Enter to change"
|
||||||
|
:maxlength="200" />
|
||||||
|
</b-field>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</div><!-- auth -->
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-4">
|
||||||
|
<b-field label="Max. connections" label-position="on-border"
|
||||||
|
message="Maximum concurrent connections to the server.">
|
||||||
|
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light"
|
||||||
|
controls-position="compact"
|
||||||
|
placeholder="25" min="1" max="65535" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<b-field label="Retries" label-position="on-border"
|
||||||
|
message="Number of times to rety when a message fails.">
|
||||||
|
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
||||||
|
type="is-light"
|
||||||
|
controls-position="compact"
|
||||||
|
placeholder="2" min="1" max="1000" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<b-field label="Request imeout" label-position="on-border"
|
||||||
|
message="Request timeout duration (s for second, m for minute).">
|
||||||
|
<b-input v-model="item.timeout" name="timeout"
|
||||||
|
placeholder="5s" :pattern="regDuration" :maxlength="10" />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div><!-- second container column -->
|
||||||
|
</div><!-- block -->
|
||||||
|
</div><!-- mail-servers -->
|
||||||
|
|
||||||
|
<b-button @click="addMessenger" icon-left="plus" type="is-primary">Add new</b-button>
|
||||||
|
</b-tab-item><!-- messengers -->
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
@ -421,6 +508,24 @@ export default Vue.extend({
|
||||||
this.form.smtp.splice(i, 1, s);
|
this.form.smtp.splice(i, 1, s);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addMessenger() {
|
||||||
|
this.form.messengers.push({
|
||||||
|
enabled: true,
|
||||||
|
root_url: '',
|
||||||
|
name: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
max_conns: 25,
|
||||||
|
max_msg_retries: 2,
|
||||||
|
timeout: '5s',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMessenger(i) {
|
||||||
|
this.form.messengers.splice(i, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
const form = JSON.parse(JSON.stringify(this.form));
|
const form = JSON.parse(JSON.stringify(this.form));
|
||||||
|
|
||||||
|
@ -442,6 +547,13 @@ export default Vue.extend({
|
||||||
form['upload.s3.aws_secret_access_key'] = '';
|
form['upload.s3.aws_secret_access_key'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < form.messengers.length; i += 1) {
|
||||||
|
// If it's the dummy UI password placeholder, ignore it.
|
||||||
|
if (form.messengers[i].password === dummyPassword) {
|
||||||
|
form.messengers[i].password = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.$api.updateSettings(form).then((data) => {
|
this.$api.updateSettings(form).then((data) => {
|
||||||
if (data.needsRestart) {
|
if (data.needsRestart) {
|
||||||
|
@ -461,6 +573,7 @@ export default Vue.extend({
|
||||||
this.$api.getHealth().then(() => {
|
this.$api.getHealth().then(() => {
|
||||||
clearInterval(pollId);
|
clearInterval(pollId);
|
||||||
this.getSettings();
|
this.getSettings();
|
||||||
|
this.$reloadServerConfig();
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -480,6 +593,12 @@ export default Vue.extend({
|
||||||
d.smtp[i].password = dummyPassword;
|
d.smtp[i].password = dummyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < d.messengers.length; i += 1) {
|
||||||
|
// The backend doesn't send passwords, so add a dummy so that it
|
||||||
|
// the password looks filled on the UI.
|
||||||
|
d.messengers[i].password = dummyPassword;
|
||||||
|
}
|
||||||
|
|
||||||
if (d['upload.provider'] === 's3') {
|
if (d['upload.provider'] === 's3') {
|
||||||
d['upload.s3.aws_secret_access_key'] = dummyPassword;
|
d['upload.s3.aws_secret_access_key'] = dummyPassword;
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
||||||
github.com/labstack/echo v3.3.10+incompatible
|
github.com/labstack/echo v3.3.10+incompatible
|
||||||
github.com/labstack/gommon v0.3.0 // indirect
|
github.com/labstack/gommon v0.3.0 // indirect
|
||||||
github.com/lib/pq v1.3.0
|
github.com/lib/pq v1.3.0
|
||||||
|
github.com/mailru/easyjson v0.7.6
|
||||||
github.com/nats-io/nats-server/v2 v2.1.7 // indirect
|
github.com/nats-io/nats-server/v2 v2.1.7 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -34,6 +34,8 @@ github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSj
|
||||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8 h1:HVq7nA5uWjpo93WsWjva1YIBuQrr8UkWQEUbzg1DX+E=
|
github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8 h1:HVq7nA5uWjpo93WsWjva1YIBuQrr8UkWQEUbzg1DX+E=
|
||||||
github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||||
github.com/knadh/goyesql v2.0.0+incompatible h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk=
|
github.com/knadh/goyesql v2.0.0+incompatible h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk=
|
||||||
|
@ -63,6 +65,8 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
|
|
@ -74,10 +74,10 @@ type CampaignMessage struct {
|
||||||
|
|
||||||
// Message represents a generic message to be pushed to a messenger.
|
// Message represents a generic message to be pushed to a messenger.
|
||||||
type Message struct {
|
type Message struct {
|
||||||
From string
|
messenger.Message
|
||||||
To []string
|
Subscriber models.Subscriber
|
||||||
Subject string
|
|
||||||
Body []byte
|
// Messenger is the messenger backend to use: email|postback.
|
||||||
Messenger string
|
Messenger string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,15 +171,6 @@ func (m *Manager) PushMessage(msg Message) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessengerNames returns the list of registered messengers.
|
|
||||||
func (m *Manager) GetMessengerNames() []string {
|
|
||||||
names := make([]string, 0, len(m.messengers))
|
|
||||||
for n := range m.messengers {
|
|
||||||
names = append(names, n)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasMessenger checks if a given messenger is registered.
|
// HasMessenger checks if a given messenger is registered.
|
||||||
func (m *Manager) HasMessenger(id string) bool {
|
func (m *Manager) HasMessenger(id string) bool {
|
||||||
_, ok := m.messengers[id]
|
_, ok := m.messengers[id]
|
||||||
|
@ -256,7 +247,10 @@ func (m *Manager) messageWorker() {
|
||||||
From: msg.from,
|
From: msg.from,
|
||||||
To: []string{msg.to},
|
To: []string{msg.to},
|
||||||
Subject: msg.subject,
|
Subject: msg.subject,
|
||||||
|
ContentType: msg.Campaign.ContentType,
|
||||||
Body: msg.body,
|
Body: msg.body,
|
||||||
|
Subscriber: msg.Subscriber,
|
||||||
|
Campaign: msg.Campaign,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach List-Unsubscribe headers?
|
// Attach List-Unsubscribe headers?
|
||||||
|
@ -267,7 +261,7 @@ func (m *Manager) messageWorker() {
|
||||||
out.Headers = h
|
out.Headers = h
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.messengers[msg.Campaign.MessengerID].Push(out); err != nil {
|
if err := m.messengers[msg.Campaign.Messenger].Push(out); err != nil {
|
||||||
m.logger.Printf("error sending message in campaign %s: %v", msg.Campaign.Name, err)
|
m.logger.Printf("error sending message in campaign %s: %v", msg.Campaign.Name, err)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
@ -286,7 +280,10 @@ func (m *Manager) messageWorker() {
|
||||||
From: msg.From,
|
From: msg.From,
|
||||||
To: msg.To,
|
To: msg.To,
|
||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
|
ContentType: msg.ContentType,
|
||||||
Body: msg.Body,
|
Body: msg.Body,
|
||||||
|
Subscriber: msg.Subscriber,
|
||||||
|
Campaign: msg.Campaign,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Printf("error sending message '%s': %v", msg.Subject, err)
|
m.logger.Printf("error sending message '%s': %v", msg.Subject, err)
|
||||||
|
@ -394,9 +391,9 @@ func (m *Manager) scanCampaigns(tick time.Duration) {
|
||||||
// addCampaign adds a campaign to the process queue.
|
// addCampaign adds a campaign to the process queue.
|
||||||
func (m *Manager) addCampaign(c *models.Campaign) error {
|
func (m *Manager) addCampaign(c *models.Campaign) error {
|
||||||
// Validate messenger.
|
// Validate messenger.
|
||||||
if _, ok := m.messengers[c.MessengerID]; !ok {
|
if _, ok := m.messengers[c.Messenger]; !ok {
|
||||||
m.src.UpdateCampaignStatus(c.ID, models.CampaignStatusCancelled)
|
m.src.UpdateCampaignStatus(c.ID, models.CampaignStatusCancelled)
|
||||||
return fmt.Errorf("unknown messenger %s on campaign %s", c.MessengerID, c.Name)
|
return fmt.Errorf("unknown messenger %s on campaign %s", c.Messenger, c.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the template.
|
// Load the template.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package messenger
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
|
||||||
"github.com/jaytaylor/html2text"
|
"github.com/jaytaylor/html2text"
|
||||||
|
"github.com/knadh/listmonk/internal/messenger"
|
||||||
"github.com/knadh/smtppool"
|
"github.com/knadh/smtppool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,9 +36,8 @@ type Emailer struct {
|
||||||
servers []*Server
|
servers []*Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEmailer creates and returns an e-mail Messenger backend.
|
// New returns an SMTP e-mail Messenger backend with a the given SMTP servers.
|
||||||
// It takes multiple SMTP configurations.
|
func New(servers ...Server) (*Emailer, error) {
|
||||||
func NewEmailer(servers ...Server) (*Emailer, error) {
|
|
||||||
e := &Emailer{
|
e := &Emailer{
|
||||||
servers: make([]*Server, 0, len(servers)),
|
servers: make([]*Server, 0, len(servers)),
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ func (e *Emailer) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push pushes a message to the server.
|
// Push pushes a message to the server.
|
||||||
func (e *Emailer) Push(m Message) error {
|
func (e *Emailer) Push(m messenger.Message) error {
|
||||||
// If there are more than one SMTP servers, send to a random
|
// If there are more than one SMTP servers, send to a random
|
||||||
// one from the list.
|
// one from the list.
|
||||||
var (
|
var (
|
|
@ -1,6 +1,10 @@
|
||||||
package messenger
|
package messenger
|
||||||
|
|
||||||
import "net/textproto"
|
import (
|
||||||
|
"net/textproto"
|
||||||
|
|
||||||
|
"github.com/knadh/listmonk/models"
|
||||||
|
)
|
||||||
|
|
||||||
// Messenger is an interface for a generic messaging backend,
|
// Messenger is an interface for a generic messaging backend,
|
||||||
// for instance, e-mail, SMS etc.
|
// for instance, e-mail, SMS etc.
|
||||||
|
@ -16,9 +20,15 @@ type Message struct {
|
||||||
From string
|
From string
|
||||||
To []string
|
To []string
|
||||||
Subject string
|
Subject string
|
||||||
|
ContentType string
|
||||||
Body []byte
|
Body []byte
|
||||||
Headers textproto.MIMEHeader
|
Headers textproto.MIMEHeader
|
||||||
Attachments []Attachment
|
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
|
// Attachment represents a file or blob attachment that can be
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
package postback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"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"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Recipients []recipient `json:"recipients"`
|
||||||
|
Campaign *campaign `json:"campaign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type campaign struct {
|
||||||
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Tags []string `db:"tags" json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type recipient struct {
|
||||||
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
|
Email string `db:"email" json:"email"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Attribs models.SubscriberAttribs `db:"attribs" json:"attribs"`
|
||||||
|
Status string `db:"status" json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options represents HTTP Postback server options.
|
||||||
|
type Options struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
RootURL string `json:"root_url"`
|
||||||
|
MaxConns int `json:"max_conns"`
|
||||||
|
Retries int `json:"retries"`
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postback represents an HTTP Message server.
|
||||||
|
type Postback struct {
|
||||||
|
authStr string
|
||||||
|
o Options
|
||||||
|
c *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new instance of the HTTP Postback messenger.
|
||||||
|
func New(o Options) (*Postback, error) {
|
||||||
|
authStr := ""
|
||||||
|
if o.Username != "" && o.Password != "" {
|
||||||
|
authStr = fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(
|
||||||
|
[]byte(o.Username+":"+o.Password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Postback{
|
||||||
|
authStr: authStr,
|
||||||
|
o: o,
|
||||||
|
c: &http.Client{
|
||||||
|
Timeout: o.Timeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConnsPerHost: o.MaxConns,
|
||||||
|
MaxConnsPerHost: o.MaxConns,
|
||||||
|
ResponseHeaderTimeout: o.Timeout,
|
||||||
|
IdleConnTimeout: o.Timeout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the messenger's name.
|
||||||
|
func (p *Postback) Name() string {
|
||||||
|
return p.o.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push pushes a message to the server.
|
||||||
|
func (p *Postback) Push(m messenger.Message) error {
|
||||||
|
pb := postback{
|
||||||
|
Subject: m.Subject,
|
||||||
|
ContentType: m.ContentType,
|
||||||
|
Body: string(m.Body),
|
||||||
|
Recipients: []recipient{{
|
||||||
|
UUID: m.Subscriber.UUID,
|
||||||
|
Email: m.Subscriber.Email,
|
||||||
|
Name: m.Subscriber.Name,
|
||||||
|
Status: m.Subscriber.Status,
|
||||||
|
Attribs: m.Subscriber.Attribs,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Campaign != nil {
|
||||||
|
pb.Campaign = &campaign{
|
||||||
|
UUID: m.Campaign.UUID,
|
||||||
|
Name: m.Campaign.Name,
|
||||||
|
Tags: m.Campaign.Tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := pb.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.exec(http.MethodPost, p.o.RootURL, b, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush flushes the message queue to the server.
|
||||||
|
func (p *Postback) Flush() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes idle HTTP connections.
|
||||||
|
func (p *Postback) Close() error {
|
||||||
|
p.c.CloseIdleConnections()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postback) exec(method, rURL string, reqBody []byte, headers http.Header) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
postBody io.Reader
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode POST / PUT params.
|
||||||
|
if method == http.MethodPost || method == http.MethodPut {
|
||||||
|
postBody = bytes.NewReader(reqBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, rURL, postBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers != nil {
|
||||||
|
req.Header = headers
|
||||||
|
} else {
|
||||||
|
req.Header = http.Header{}
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "listmonk")
|
||||||
|
|
||||||
|
// Optional BasicAuth.
|
||||||
|
if p.authStr != "" {
|
||||||
|
req.Header.Set("Authorization", p.authStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a content-type isn't set, set the default one.
|
||||||
|
if req.Header.Get("Content-Type") == "" {
|
||||||
|
if method == http.MethodPost || method == http.MethodPut {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request method is GET or DELETE, add the params as QueryString.
|
||||||
|
if method == http.MethodGet || method == http.MethodDelete {
|
||||||
|
req.URL.RawQuery = string(reqBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := p.c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// Drain and close the body to let the Transport reuse the connection
|
||||||
|
io.Copy(ioutil.Discard, r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if r.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("non-OK response from Postback server: %d", r.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,358 @@
|
||||||
|
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||||
|
|
||||||
|
package postback
|
||||||
|
|
||||||
|
import (
|
||||||
|
json "encoding/json"
|
||||||
|
models "github.com/knadh/listmonk/models"
|
||||||
|
easyjson "github.com/mailru/easyjson"
|
||||||
|
jlexer "github.com/mailru/easyjson/jlexer"
|
||||||
|
jwriter "github.com/mailru/easyjson/jwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppress unused package warning
|
||||||
|
var (
|
||||||
|
_ *json.RawMessage
|
||||||
|
_ *jlexer.Lexer
|
||||||
|
_ *jwriter.Writer
|
||||||
|
_ easyjson.Marshaler
|
||||||
|
)
|
||||||
|
|
||||||
|
func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback(in *jlexer.Lexer, out *postback) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeFieldName(false)
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "subject":
|
||||||
|
out.Subject = string(in.String())
|
||||||
|
case "content_type":
|
||||||
|
out.ContentType = string(in.String())
|
||||||
|
case "body":
|
||||||
|
out.Body = string(in.String())
|
||||||
|
case "recipients":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Recipients = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Recipients == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Recipients = make([]recipient, 0, 0)
|
||||||
|
} else {
|
||||||
|
out.Recipients = []recipient{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Recipients = (out.Recipients)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v1 recipient
|
||||||
|
easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback1(in, &v1)
|
||||||
|
out.Recipients = append(out.Recipients, v1)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
|
case "campaign":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Campaign = nil
|
||||||
|
} else {
|
||||||
|
if out.Campaign == nil {
|
||||||
|
out.Campaign = new(campaign)
|
||||||
|
}
|
||||||
|
easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback2(in, out.Campaign)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback(out *jwriter.Writer, in postback) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
{
|
||||||
|
const prefix string = ",\"subject\":"
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
out.String(string(in.Subject))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"content_type\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.ContentType))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"body\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.Body))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"recipients\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
if in.Recipients == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
|
||||||
|
out.RawString("null")
|
||||||
|
} else {
|
||||||
|
out.RawByte('[')
|
||||||
|
for v2, v3 := range in.Recipients {
|
||||||
|
if v2 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback1(out, v3)
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"campaign\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
if in.Campaign == nil {
|
||||||
|
out.RawString("null")
|
||||||
|
} else {
|
||||||
|
easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback2(out, *in.Campaign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON supports json.Marshaler interface
|
||||||
|
func (v postback) MarshalJSON() ([]byte, error) {
|
||||||
|
w := jwriter.Writer{}
|
||||||
|
easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback(&w, v)
|
||||||
|
return w.Buffer.BuildBytes(), w.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
|
func (v postback) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback(w, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON supports json.Unmarshaler interface
|
||||||
|
func (v *postback) UnmarshalJSON(data []byte) error {
|
||||||
|
r := jlexer.Lexer{Data: data}
|
||||||
|
easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback(&r, v)
|
||||||
|
return r.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
|
func (v *postback) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback(l, v)
|
||||||
|
}
|
||||||
|
func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback2(in *jlexer.Lexer, out *campaign) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeFieldName(false)
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "uuid":
|
||||||
|
out.UUID = string(in.String())
|
||||||
|
case "name":
|
||||||
|
out.Name = string(in.String())
|
||||||
|
case "tags":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Tags = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Tags == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Tags = make([]string, 0, 4)
|
||||||
|
} else {
|
||||||
|
out.Tags = []string{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Tags = (out.Tags)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v4 string
|
||||||
|
v4 = string(in.String())
|
||||||
|
out.Tags = append(out.Tags, v4)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback2(out *jwriter.Writer, in campaign) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
{
|
||||||
|
const prefix string = ",\"uuid\":"
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
out.String(string(in.UUID))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"name\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.Name))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"tags\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
|
||||||
|
out.RawString("null")
|
||||||
|
} else {
|
||||||
|
out.RawByte('[')
|
||||||
|
for v5, v6 := range in.Tags {
|
||||||
|
if v5 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
out.String(string(v6))
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback1(in *jlexer.Lexer, out *recipient) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeFieldName(false)
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "uuid":
|
||||||
|
out.UUID = string(in.String())
|
||||||
|
case "email":
|
||||||
|
out.Email = string(in.String())
|
||||||
|
case "name":
|
||||||
|
out.Name = string(in.String())
|
||||||
|
case "attribs":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
} else {
|
||||||
|
in.Delim('{')
|
||||||
|
out.Attribs = make(models.SubscriberAttribs)
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := string(in.String())
|
||||||
|
in.WantColon()
|
||||||
|
var v7 interface{}
|
||||||
|
if m, ok := v7.(easyjson.Unmarshaler); ok {
|
||||||
|
m.UnmarshalEasyJSON(in)
|
||||||
|
} else if m, ok := v7.(json.Unmarshaler); ok {
|
||||||
|
_ = m.UnmarshalJSON(in.Raw())
|
||||||
|
} else {
|
||||||
|
v7 = in.Interface()
|
||||||
|
}
|
||||||
|
(out.Attribs)[key] = v7
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
}
|
||||||
|
case "status":
|
||||||
|
out.Status = string(in.String())
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback1(out *jwriter.Writer, in recipient) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
{
|
||||||
|
const prefix string = ",\"uuid\":"
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
out.String(string(in.UUID))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"email\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.Email))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"name\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.Name))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"attribs\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
if in.Attribs == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 {
|
||||||
|
out.RawString(`null`)
|
||||||
|
} else {
|
||||||
|
out.RawByte('{')
|
||||||
|
v8First := true
|
||||||
|
for v8Name, v8Value := range in.Attribs {
|
||||||
|
if v8First {
|
||||||
|
v8First = false
|
||||||
|
} else {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
out.String(string(v8Name))
|
||||||
|
out.RawByte(':')
|
||||||
|
if m, ok := v8Value.(easyjson.Marshaler); ok {
|
||||||
|
m.MarshalEasyJSON(out)
|
||||||
|
} else if m, ok := v8Value.(json.Marshaler); ok {
|
||||||
|
out.Raw(m.MarshalJSON())
|
||||||
|
} else {
|
||||||
|
out.Raw(json.Marshal(v8Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"status\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
out.String(string(in.Status))
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
|
@ -164,7 +164,7 @@ type Campaign struct {
|
||||||
ContentType string `db:"content_type" json:"content_type"`
|
ContentType string `db:"content_type" json:"content_type"`
|
||||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||||
TemplateID int `db:"template_id" json:"template_id"`
|
TemplateID int `db:"template_id" json:"template_id"`
|
||||||
MessengerID string `db:"messenger" json:"messenger"`
|
Messenger string `db:"messenger" json:"messenger"`
|
||||||
|
|
||||||
// TemplateBody is joined in from templates by the next-campaigns query.
|
// TemplateBody is joined in from templates by the next-campaigns query.
|
||||||
TemplateBody string `db:"template_body" json:"-"`
|
TemplateBody string `db:"template_body" json:"-"`
|
||||||
|
|
|
@ -561,16 +561,17 @@ WITH camp AS (
|
||||||
send_at=(CASE WHEN $8 THEN $7::TIMESTAMP WITH TIME ZONE WHEN NOT $8 THEN NULL ELSE send_at END),
|
send_at=(CASE WHEN $8 THEN $7::TIMESTAMP WITH TIME ZONE WHEN NOT $8 THEN NULL ELSE send_at END),
|
||||||
status=(CASE WHEN NOT $8 THEN 'draft' ELSE status END),
|
status=(CASE WHEN NOT $8 THEN 'draft' ELSE status END),
|
||||||
tags=$9::VARCHAR(100)[],
|
tags=$9::VARCHAR(100)[],
|
||||||
template_id=(CASE WHEN $10 != 0 THEN $10 ELSE template_id END),
|
messenger=(CASE WHEN $10 != '' THEN $10 ELSE messenger END),
|
||||||
|
template_id=(CASE WHEN $11 != 0 THEN $11 ELSE template_id END),
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
WHERE id = $1 RETURNING id
|
WHERE id = $1 RETURNING id
|
||||||
),
|
),
|
||||||
d AS (
|
d AS (
|
||||||
-- Reset list relationships
|
-- Reset list relationships
|
||||||
DELETE FROM campaign_lists WHERE campaign_id = $1 AND NOT(list_id = ANY($11))
|
DELETE FROM campaign_lists WHERE campaign_id = $1 AND NOT(list_id = ANY($12))
|
||||||
)
|
)
|
||||||
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
(SELECT $1 as campaign_id, id, name FROM lists WHERE id=ANY($11::INT[]))
|
(SELECT $1 as campaign_id, id, name FROM lists WHERE id=ANY($12::INT[]))
|
||||||
ON CONFLICT (campaign_id, list_id) DO UPDATE SET list_name = EXCLUDED.list_name;
|
ON CONFLICT (campaign_id, list_id) DO UPDATE SET list_name = EXCLUDED.list_name;
|
||||||
|
|
||||||
-- name: update-campaign-counts
|
-- name: update-campaign-counts
|
||||||
|
|
Loading…
Reference in New Issue