Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple SMTP servers #2290

Merged
merged 7 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
knadh marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 != "" {
knadh marked this conversation as resolved.
Show resolved Hide resolved
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
22 changes: 17 additions & 5 deletions cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,29 @@ 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.
s.Name = reAlphaNum.ReplaceAllString(strings.ToLower(strings.TrimSpace(s.Name)), "")
if s.Name != "" {
if _, ok := names[s.Name]; ok {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("settings.duplicateMessengerName", "name", s.Name))
}

names[s.Name] = true
}

// 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 +176,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 +309,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">
knadh marked this conversation as resolved.
Show resolved Hide resolved
<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('settings.mailserver.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 del servidor SMTP. Es pot utilitzar a la configuració de la campanya.",
"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ý název serveru SMTP. Použitelné v nastavení kampaně.",
"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 dewisol y gweinydd SMTP. Gellir ei ddefnyddio yng ngosodiadau'r ymgyrch.",
"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 navn på SMTP-serveren. Kan bruges i kampagneindstillingerne.",
"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 Name des SMTP-Servers. Verwendbar in den Kampagneneinstellungen.",
"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. Μπορεί να χρησιμοποιηθεί στις ρυθμίσεις καμπάνιας.",
"settings.mailserver.password": "Κωδικός πρόσβασης",
"settings.mailserver.passwordHelp": "Enter για να το αλλάξετε",
"settings.mailserver.port": "Θύρα",
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@
"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.name": "Name",
"settings.mailserver.nameHelp": "Optional unique name for the SMTP server. Setting this allows the server to be specifically selected for a campaign.",
knadh marked this conversation as resolved.
Show resolved Hide resolved
"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": "Laŭvola nomo de la SMTP-servilo. Uzebla en la agordoj de kampanjo.",
"settings.mailserver.password": "Contrasenya",
"settings.mailserver.passwordHelp": "Fes intro per canviar",
"settings.mailserver.port": "Pordo",
Expand Down
Loading