Skip to content

Commit

Permalink
Add support for selecting SMTP per campaign (#2290)
Browse files Browse the repository at this point in the history
This patch adds a new optional `name` field to SMTP server config on the UI.
When a name is given to an SMTP server, it's initialized as a standalone messenger
which shows up as a sub-group item under the main "email" messenger
on the campaign page.

Co-authored-by: Kailash Nadh <[email protected]>
  • Loading branch information
lcd1232 and knadh authored Feb 11, 2025
1 parent 756e391 commit d055cc5
Show file tree
Hide file tree
Showing 42 changed files with 188 additions and 72 deletions.
14 changes: 3 additions & 11 deletions cmd/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"syscall"
"time"

Expand Down Expand Up @@ -45,17 +44,10 @@ func handleGetServerConfig(c echo.Context) error {
}
out.Langs = langList

// 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)
out.Messengers = make([]string, 0, len(app.messengers))
for _, m := range app.messengers {
out.Messengers = append(out.Messengers, m.Name())
}
sort.Strings(names)
out.Messengers = append(out.Messengers, emailMsgr)
out.Messengers = append(out.Messengers, names...)

app.Lock()
out.NeedsRestart = app.needsRestart
Expand Down
49 changes: 29 additions & 20 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,20 +548,15 @@ func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *su
}, db.DB, app.i18n)
}

// initSMTPMessenger initializes the SMTP messenger.
func initSMTPMessenger(m *manager.Manager) manager.Messenger {
// initSMTPMessenger initializes the combined and individual SMTP messengers.
func initSMTPMessengers() []manager.Messenger {
var (
mapKeys = ko.MapKeys("smtp")
servers = make([]email.Server, 0, len(mapKeys))
servers = []email.Server{}
out = []manager.Messenger{}
)

items := ko.Slices("smtp")
if len(items) == 0 {
lo.Fatalf("no SMTP servers found in config")
}

// Load the config for multiple SMTP servers.
for _, item := range items {
for _, item := range ko.Slices("smtp") {
if !item.Bool("enabled") {
continue
}
Expand All @@ -573,25 +568,39 @@ func initSMTPMessenger(m *manager.Manager) manager.Messenger {
}

servers = append(servers, s)
lo.Printf("loaded email (SMTP) messenger: %s@%s",
item.String("username"), item.String("host"))
}
if len(servers) == 0 {
lo.Fatalf("no SMTP servers enabled in settings")
lo.Printf("initialized email (SMTP) messenger: %s@%s", item.String("username"), item.String("host"))

// If the server has a name, initialize it as a standalone e-mail messenger
// allowing campaigns to select individual SMTPs. In the UI and config, it'll appear as `email / $name`.
if s.Name != "" {
msgr, err := email.New(fmt.Sprintf("%s / %s", email.MessengerName, s.Name), s)
if err != nil {
lo.Fatalf("error initializing e-mail messenger: %v", err)
}
out = append(out, msgr)
}
}

// Initialize the e-mail messenger with multiple SMTP servers.
msgr, err := email.New(servers...)
// Initialize the 'email' messenger with all SMTP servers.
msgr, err := email.New(email.MessengerName, servers...)
if err != nil {
lo.Fatalf("error loading e-mail messenger: %v", err)
lo.Fatalf("error initializing e-mail messenger: %v", err)
}

// If it's just one server, return the default "email" messenger.
if len(servers) == 1 {
return []manager.Messenger{msgr}
}

return msgr
// If there are multiple servers, prepend the group "email" to be the first one.
out = append([]manager.Messenger{msgr}, out...)

return out
}

// initPostbackMessengers initializes and returns all the enabled
// HTTP postback messenger backends.
func initPostbackMessengers(m *manager.Manager) []manager.Messenger {
func initPostbackMessengers() []manager.Messenger {
items := ko.Slices("messengers")
if len(items) == 0 {
return nil
Expand Down
54 changes: 29 additions & 25 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,26 @@ const (
// App contains the "global" components that are
// passed around, especially through HTTP handlers.
type App struct {
core *core.Core
fs stuffbin.FileSystem
db *sqlx.DB
queries *models.Queries
constants *constants
manager *manager.Manager
importer *subimporter.Importer
messengers map[string]manager.Messenger
auth *auth.Auth
media media.Store
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
captcha *captcha.Captcha
events *events.Events
notifTpls *notifTpls
about about
log *log.Logger
bufLog *buflog.BufLog
core *core.Core
fs stuffbin.FileSystem
db *sqlx.DB
queries *models.Queries
constants *constants
manager *manager.Manager
importer *subimporter.Importer
messengers []manager.Messenger
emailMessenger manager.Messenger
auth *auth.Auth
media media.Store
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
captcha *captcha.Captcha
events *events.Events
notifTpls *notifTpls
about about
log *log.Logger
bufLog *buflog.BufLog

// Channel for passing reload signals.
chReload chan os.Signal
Expand Down Expand Up @@ -175,7 +176,7 @@ func main() {
db: db,
constants: initConstants(),
media: initMediaStore(),
messengers: make(map[string]manager.Messenger),
messengers: []manager.Messenger{},
log: lo,
bufLog: bufLog,
captcha: initCaptcha(),
Expand Down Expand Up @@ -230,13 +231,16 @@ func main() {
go app.bounce.Run()
}

// Initialize the default SMTP (`email`) messenger.
app.messengers[emailMsgr] = initSMTPMessenger(app.manager)
// Initialize the SMTP messengers.
app.messengers = initSMTPMessengers()
for _, m := range app.messengers {
if m.Name() == emailMsgr {
app.emailMessenger = m
}
}

// Initialize any additional postback messengers.
for _, m := range initPostbackMessengers(app.manager) {
app.messengers[m.Name()] = m
}
app.messengers = append(app.messengers, initPostbackMessengers()...)

// Attach all messengers to the campaign manager.
for _, m := range app.messengers {
Expand Down
2 changes: 1 addition & 1 deletion cmd/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ 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(models.Message{
if err := app.emailMessenger.Push(models.Message{
ContentType: app.notifTpls.contentType,
From: app.constants.FromEmail,
To: []string{data.Email},
Expand Down
23 changes: 18 additions & 5 deletions cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,30 @@ func handleUpdateSettings(c echo.Context) error {
return err
}

// Validate and sanitize postback Messenger names along with SMTP names
// (where each SMTP is also considered as a standalone messenger).
// Duplicates are disallowed and "email" is a reserved name.
names := map[string]bool{emailMsgr: true}

// There should be at least one SMTP block that's enabled.
has := false
for i, s := range set.SMTP {
if s.Enabled {
has = true
}

// Sanitize and normalize the SMTP server name.
name := reAlphaNum.ReplaceAllString(strings.ToLower(strings.TrimSpace(s.Name)), "-")
if name != "" {
if _, ok := names[name]; ok {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("settings.duplicateMessengerName", "name", name))
}

names[name] = true
}
set.SMTP[i].Name = name

// Assign a UUID. The frontend only sends a password when the user explicitly
// changes the password. In other cases, the existing password in the DB
// is copied while updating the settings and the UUID is used to match
Expand Down Expand Up @@ -160,10 +177,6 @@ func handleUpdateSettings(c echo.Context) error {
}
}

// Validate and sanitize postback Messenger names. Duplicates are disallowed
// and "email" is a reserved name.
names := map[string]bool{emailMsgr: true}

for i, m := range set.Messengers {
// UUID to keep track of password changes similar to the SMTP logic above.
if m.UUID == "" {
Expand Down Expand Up @@ -297,7 +310,7 @@ func handleTestSMTPSettings(c echo.Context) error {
req.MaxConns = 1
req.IdleTimeout = time.Second * 2
req.PoolWaitTimeout = time.Second * 2
msgr, err := email.New(req)
msgr, err := email.New("", req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.errorCreating", "name", "SMTP", "error", err.Error()))
Expand Down
4 changes: 4 additions & 0 deletions docs/swagger/collections.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2880,6 +2880,10 @@ components:
type: string
settings.mailserver.authProtocol:
type: string
settings.mailserver.name:
type: string
settings.mailserver.nameHelp:
type: string
settings.mailserver.host:
type: string
settings.mailserver.hostHelp:
Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
},
"resolutions": {
"jackspeak": "2.1.1"
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
20 changes: 17 additions & 3 deletions frontend/src/views/Campaign.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger" name="messenger"
:disabled="!canEdit" required>
<option v-for="m in messengers" :value="m" :key="m">
<template v-if="emailMessengers.length > 1">
<optgroup label="email">
<option v-for="m in emailMessengers" :value="m" :key="m">
{{ m }}
</option>
</optgroup>
</template>
<template v-else>
<option value="email">email</option>
</template>
<option v-for="m in otherMessengers" :value="m" :key="m">
{{ m }}
</option>
</b-select>
Expand Down Expand Up @@ -640,8 +650,12 @@ export default Vue.extend({
return this.lists.results.filter((l) => this.selListIDs.indexOf(l.id) > -1);
},
messengers() {
return [...this.serverConfig.messengers];
emailMessengers() {
return ['email', ...this.serverConfig.messengers.filter((m) => m.startsWith('email /'))];
},
otherMessengers() {
return this.serverConfig.messengers.filter((m) => m !== 'email' && !m.startsWith('email /'));
},
},
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/views/settings/smtp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<div class="column" :class="{ disabled: !item.enabled }">
<div class="columns">
<div class="column is-8">
<div class="column is-9">
<b-field :label="$t('settings.mailserver.host')" label-position="on-border"
:message="$t('settings.mailserver.hostHelp')">
<b-input v-model="item.host" name="host" placeholder="smtp.yourmailserver.net" :maxlength="200" />
Expand Down Expand Up @@ -141,6 +141,15 @@
</div>
</div>

<div class="columns">
<div class="column is-6">
<b-field :label="$t('globals.fields.name')" label-position="on-border"
:message="$t('settings.mailserver.nameHelp')">
<b-input v-model="item.name" name="name" placeholder="" :maxlength="100" />
</b-field>
</div>
</div>

<div class="columns">
<div class="column">
<p v-if="item.email_headers.length === 0 && !item.showHeaders">
Expand Down Expand Up @@ -251,6 +260,7 @@ export default Vue.extend({
methods: {
addSMTP() {
this.data.smtp.push({
name: '',
enabled: true,
host: '',
hello_hostname: '',
Expand Down
2 changes: 2 additions & 0 deletions i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@
"settings.mailserver.idleTimeoutHelp": "Temps d'inactivitat per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la de la grup (s per segon, m per minut).",
"settings.mailserver.maxConns": "Connexions màximes",
"settings.mailserver.maxConnsHelp": "Màxim de connexions concurrents al servidor.",
"settings.mailserver.name": "Nom",
"settings.mailserver.nameHelp": "Nom opcional únic per al servidor SMTP. Establir això permet seleccionar específicament el servidor per a una campanya. Exemple: primary-server. Alfanumèric / guionet.",
"settings.mailserver.password": "Contrasenya",
"settings.mailserver.passwordHelp": "Fes intro per canviar",
"settings.mailserver.port": "Port",
Expand Down
2 changes: 2 additions & 0 deletions i18n/cs-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@
"settings.mailserver.idleTimeoutHelp": "Doba čekání na novou aktivitu na připojení před uzavřením a odebráním z fondu (s - sekundy, m - minuty).",
"settings.mailserver.maxConns": "Maximální počet připojení",
"settings.mailserver.maxConnsHelp": "Maximální počet souběžných připojení k serveru.",
"settings.mailserver.name": "Jméno",
"settings.mailserver.nameHelp": "Volitelný jedinečný název pro SMTP server. Nastavením tohoto parametru je možné vybrat daný server pro danou kampaň. Např.: primary-server. Alfanumerické / pomlčka.",
"settings.mailserver.password": "Heslo",
"settings.mailserver.passwordHelp": "Klávesou Enter zadejte změnu",
"settings.mailserver.port": "Port",
Expand Down
2 changes: 2 additions & 0 deletions i18n/cy.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@
"settings.mailserver.idleTimeoutHelp": "Amser aros ar gyfer gweithgaredd newydd ar gysylltiad cyn ei gau a'i ddileu o'r gronfa (e ar gyfer eiliad",
"settings.mailserver.maxConns": "Uchafswm nifer y cysylltiadau",
"settings.mailserver.maxConnsHelp": "Uchafswm nifer y cysylltiadau cydamserol â'r gweinydd",
"settings.mailserver.name": "Enw",
"settings.mailserver.nameHelp": "Enw unigryw dewisol ar gyfer y gweinydd SMTP. Trwy osod hwn, mae'n caniatáu i'r gweinydd gael ei ddewis yn benodol ar gyfer ymgyrch. e.e.: gwefr-prif. Alffaniwmerig / llinell.",
"settings.mailserver.password": "Cyfrinair",
"settings.mailserver.passwordHelp": "Pwyswch enter i'w newid",
"settings.mailserver.port": "Porth",
Expand Down
2 changes: 2 additions & 0 deletions i18n/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@
"settings.mailserver.idleTimeoutHelp": "Tid til at vente på ny aktivitet på en forbindelse, før du lukker den og fjerner den fra poolen (s for sekund, m for minut).",
"settings.mailserver.maxConns": "Maks. tilslutninger",
"settings.mailserver.maxConnsHelp": "Maksimalt antal samtidige forbindelser til serveren.",
"settings.mailserver.name": "Navn",
"settings.mailserver.nameHelp": "Valgfrit unikt navn til SMTP-serveren. Ved at angive dette kan serveren vælges specifikt til en kampagne. f.eks.: primær-server. Alfanumerisk / bindestreg.",
"settings.mailserver.password": "Kodeord",
"settings.mailserver.passwordHelp": "Indtast for at ændre",
"settings.mailserver.port": "Port",
Expand Down
2 changes: 2 additions & 0 deletions i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@
"settings.mailserver.idleTimeoutHelp": "Wartezeit auf neue Aktivität bevor eine Verbindung geschlossen und aus dem Pool entfernt wird. (s für Sekunden, m für Minuten).",
"settings.mailserver.maxConns": "Max. Verbindungen",
"settings.mailserver.maxConnsHelp": "Maximale gleichzeitige Verbindungen zum SMTP Server",
"settings.mailserver.name": "Name",
"settings.mailserver.nameHelp": "Optionaler eindeutiger Name für den SMTP-Server. Durch das Festlegen dieses Namens kann der Server speziell für eine Kampagne ausgewählt werden, z.B. primärer-server. Alphanumerisch / Bindestrich.",
"settings.mailserver.password": "Passwort",
"settings.mailserver.passwordHelp": "Gib dein Passwort ein, um es zu ändern",
"settings.mailserver.port": "Port",
Expand Down
2 changes: 2 additions & 0 deletions i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@
"settings.mailserver.idleTimeoutHelp": "Χρόνος αναμονής για νέα δραστηριότητα σε μια σύνδεση πριν από το κλείσιμό της και την αφαίρεσή της από τη δεξαμενή (s για το δευτερόλεπτο, m για το λεπτό).",
"settings.mailserver.maxConns": "Μέγιστες συνδέσεις",
"settings.mailserver.maxConnsHelp": "Μέγιστες ταυτόχρονες συνδέσεις στο διακομιστή.",
"settings.mailserver.name": "Ονομα",
"settings.mailserver.nameHelp": "Προαιρετικό μοναδικό όνομα για τον διακομιστή SMTP. Η ρύθμιση αυτή επιτρέπει την επιλογή συγκεκριμένου διακομιστή για μία καμπάνια. π.χ.: primary-server. Αλφαριθμητικά / παύλα.",
"settings.mailserver.password": "Κωδικός πρόσβασης",
"settings.mailserver.passwordHelp": "Enter για να το αλλάξετε",
"settings.mailserver.port": "Θύρα",
Expand Down
1 change: 1 addition & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@
"settings.mailserver.idleTimeoutHelp": "Time to wait for new activity on a connection before closing it and removing it from the pool (s for second, m for minute).",
"settings.mailserver.maxConns": "Max. connections",
"settings.mailserver.maxConnsHelp": "Maximum concurrent connections to the server.",
"settings.mailserver.nameHelp": "Optional unique name for the SMTP server. Setting this allows the server to be specifically selected for a campaign. eg: primary-server. Alphanumeric / dash.",
"settings.mailserver.password": "Password",
"settings.mailserver.passwordHelp": "Enter to change",
"settings.mailserver.port": "Port",
Expand Down
2 changes: 2 additions & 0 deletions i18n/eo.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@
"settings.mailserver.idleTimeoutHelp": "Temps d'inactivitat per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la de la grup (s per segon, m per minut).",
"settings.mailserver.maxConns": "Connexions màximes",
"settings.mailserver.maxConnsHelp": "Màxim de connexions concurrents al servidor.",
"settings.mailserver.name": "Nomo",
"settings.mailserver.nameHelp": "Opcia unika nomo por la SMTP-servilo. La difino permesas elekti la servilon specife por kampanjo. Ekz.: primara-servilo. Alfa-numera / streko.",
"settings.mailserver.password": "Contrasenya",
"settings.mailserver.passwordHelp": "Fes intro per canviar",
"settings.mailserver.port": "Pordo",
Expand Down
Loading

0 comments on commit d055cc5

Please sign in to comment.