Skip to content

Commit

Permalink
Add support for testing SMTP connections in the settings UI.
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Jul 11, 2022
1 parent 83a0e10 commit ee44817
Show file tree
Hide file tree
Showing 27 changed files with 237 additions and 26 deletions.
1 change: 1 addition & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {

g.GET("/api/settings", handleGetSettings)
g.PUT("/api/settings", handleUpdateSettings)
g.POST("/api/settings/smtp/test", handleTestSMTPSettings)
g.POST("/api/admin/reload", handleReloadApp)
g.GET("/api/logs", handleGetLogs)

Expand Down
66 changes: 66 additions & 0 deletions cmd/settings.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package main

import (
"bytes"
"io/ioutil"
"net/http"
"regexp"
"strings"
"syscall"
"time"

"github.com/gofrs/uuid"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/listmonk/internal/messenger/email"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
)
Expand Down Expand Up @@ -193,3 +200,62 @@ func handleGetLogs(c echo.Context) error {
app := c.Get("app").(*App)
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
}

// handleTestSMTPSettings returns the log entries stored in the log buffer.
func handleTestSMTPSettings(c echo.Context) error {
app := c.Get("app").(*App)

// Copy the raw JSON post body.
reqBody, err := ioutil.ReadAll(c.Request().Body)
if err != nil {
app.log.Printf("error reading SMTP test: %v", err)
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
}

// Load the JSON into koanf to parse SMTP settings properly including timestrings.
ko := koanf.New(".")
if err := ko.Load(rawbytes.Provider(reqBody), json.Parser()); err != nil {
app.log.Printf("error unmarshalling SMTP test request: %v", err)
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
}

req := email.Server{}
if err := ko.UnmarshalWithConf("", &req, koanf.UnmarshalConf{Tag: "json"}); err != nil {
app.log.Printf("error scanning SMTP test request: %v", err)
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
}

to := ko.String("email")
if to == "" {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.missingFields", "name", "email"))
}

// Initialize a new SMTP pool.
req.MaxConns = 1
req.IdleTimeout = time.Second * 2
req.PoolWaitTimeout = time.Second * 2
msgr, err := email.New(req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.errorCreating", "name", "SMTP", "error", err.Error()))
}

var b bytes.Buffer
if err := app.notifTpls.tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
app.log.Printf("error compiling notification template '%s': %v", "smtp-test", err)
return err
}

m := messenger.Message{}
m.ContentType = app.notifTpls.contentType
m.From = app.constants.FromEmail
m.To = []string{to}
m.Subject = app.i18n.T("settings.smtp.testConnection")
m.Body = b.Bytes()
if err := msgr.Push(m); err != nil {
app.log.Printf("error sending SMTP test (%s): %v", m.Subject, err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}

return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
}
3 changes: 3 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ export const getSettings = async () => http.get('/api/settings',
export const updateSettings = async (data) => http.put('/api/settings', data,
{ loading: models.settings });

export const testSMTP = async (data) => http.post('/api/settings/smtp/test', data,
{ loading: models.settings, disableToast: true });

export const getLogs = async () => http.get('/api/logs',
{ loading: models.logs, camelCase: false });

Expand Down
8 changes: 5 additions & 3 deletions frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,14 @@ export default class Utils {
});
};

prompt = (msg, inputAttrs, onConfirm, onCancel) => {
prompt = (msg, inputAttrs, onConfirm, onCancel, params) => {
const p = params || {};

Dialog.prompt({
scroll: 'keep',
message: this.escapeHTML(msg),
confirmText: this.i18n.t('globals.buttons.ok'),
cancelText: this.i18n.t('globals.buttons.cancel'),
confirmText: p.confirmText || this.i18n.t('globals.buttons.ok'),
cancelText: p.cancelText || this.i18n.t('globals.buttons.cancel'),
inputAttrs: {
type: 'string',
maxlength: 200,
Expand Down
121 changes: 100 additions & 21 deletions frontend/src/views/settings/smtp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,29 +134,63 @@
</b-field>
</div>
</div>
<hr />

<div>
<p v-if="item.email_headers.length === 0 && !item.showHeaders">
<a href="#" class="is-size-7" @click.prevent="() => showSMTPHeaders(n)">
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}</a>
</p>
<b-field v-if="item.email_headers.length > 0 || item.showHeaders"
label-position="on-border"
:message="$t('settings.smtp.customHeadersHelp')">
<b-input v-model="item.strEmailHeaders" name="email_headers" type="textarea"
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
</b-field>
<div class="columns">
<div class="column">
<p v-if="item.email_headers.length === 0 && !item.showHeaders">
<a href="#" class="is-size-7" @click.prevent="() => showSMTPHeaders(n)">
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}</a>
</p>
<b-field v-if="item.email_headers.length > 0 || item.showHeaders"
label-position="on-border"
:message="$t('settings.smtp.customHeadersHelp')">
<b-input v-model="item.strEmailHeaders" name="email_headers" type="textarea"
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
</b-field>
</div>
</div>
<hr />

<form @submit.prevent="() => doSMTPTest(item)">
<div class="columns">
<template v-if="smtpTestItem === n">
<div class="column is-5">
<strong>{{ $t('settings.general.fromEmail') }}</strong>
<br />
{{ settings['app.from_email'] }}
</div>
<div class="column is-4">
<b-field :label="$t('settings.smtp.toEmail')" label-position="on-border">
<b-input type="email" required v-model="testEmail"
:ref="'testEmailTo'" placeholder="email@site.com"
:custom-class="`test-email-${n}`" />
</b-field>
</div>
</template>
<div class="column has-text-right">
<b-button v-if="smtpTestItem === n" class="is-primary"
:disabled="isTestEnabled(item)" @click.prevent="() => doSMTPTest(item)">
{{ $t('settings.smtp.sendTest') }}
</b-button>
<a href="#" v-else class="is-primary" @click.prevent="showTestForm(n)">
<b-icon icon="rocket-launch-outline" /> {{ $t('settings.smtp.testConnection') }}
</a>
</div>
<div class="columns">
<div class="column">
</div>
</div>
</div>
<div v-if="errMsg && smtpTestItem === n">
<b-field class="mt-4" type="is-danger">
<b-input v-model="errMsg" type="textarea"
custom-class="has-text-danger is-size-6" readonly />
</b-field>
</div>
</form><!-- smtp test -->

</div>
</div><!-- second container column -->
<div class="columns">
<div class="column has-text-right">
<b-button class="is-primary" @click.prevent="testConnection(item)">
{{ $t('settings.smtp.testConnection') }}
</b-button>
</div>
</div>
</div><!-- block -->
</div><!-- mail-servers -->

Expand All @@ -168,6 +202,7 @@

<script>
import Vue from 'vue';
import { mapState } from 'vuex';
import { regDuration } from '../../constants';
export default Vue.extend({
Expand All @@ -181,6 +216,11 @@ export default Vue.extend({
return {
data: this.form,
regDuration,
// Index of the SMTP block item in the array to show the
// test form in.
smtpTestItem: null,
testEmail: '',
errMsg: '',
};
},
Expand Down Expand Up @@ -219,9 +259,48 @@ export default Vue.extend({
this.data.smtp.splice(i, 1, s);
},
testConnection(c) {
alert(c);
testConnection() {
let em = this.settings['app.from_email'].replace('>', '').split('<');
if (em.length > 1) {
em = `<${em[em.length - 1]}>`;
}
},
doSMTPTest(item) {
this.errMsg = '';
this.$api.testSMTP({ ...item, email: this.testEmail }).then(() => {
this.$utils.toast(this.$t('campaigns.testSent'));
}).catch((err) => {
if (err.response?.data?.message) {
this.errMsg = err.response.data.message;
}
});
},
showTestForm(n) {
this.smtpTestItem = n;
this.testItem = this.form.smtp[n];
this.errMsg = '';
this.$nextTick(() => {
document.querySelector(`.test-email-${n}`).focus();
});
},
isTestEnabled(item) {
if (!item.host || !item.port) {
return true;
}
if (item.auth_protocol !== 'none' && !item.password.trim()) {
return true;
}
return false;
},
},
computed: {
...mapState(['settings']),
},
});
</script>
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/knadh/go-pop3 v0.3.0
github.com/knadh/goyesql/v2 v2.1.2
github.com/knadh/koanf v1.2.3
github.com/knadh/smtppool v0.4.0
github.com/knadh/smtppool v1.0.0
github.com/knadh/stuffbin v1.1.0
github.com/labstack/echo/v4 v4.6.1
github.com/labstack/gommon v0.3.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ github.com/knadh/koanf v1.2.3 h1:2Rkr0YhhYk+4QEOm800Q3Pu0Wi87svTxM6uuEb4WhYw=
github.com/knadh/koanf v1.2.3/go.mod h1:xpPTwMhsA/aaQLAilyCCqfpEiY1gpa160AiCuWHJUjY=
github.com/knadh/smtppool v0.4.0 h1:335iXPwZ6katJVhauV4O6e8uPvvPmO6YLrfDQhb6UvE=
github.com/knadh/smtppool v0.4.0/go.mod h1:3DJHouXAgPDBz0kC50HukOsdapYSwIEfJGwuip46oCA=
github.com/knadh/smtppool v1.0.0 h1:1c8A7+nD8WdMMzvd3yY5aoY9QBgyGTA+Iq1IdlgKGJw=
github.com/knadh/smtppool v1.0.0/go.mod h1:3DJHouXAgPDBz0kC50HukOsdapYSwIEfJGwuip46oCA=
github.com/knadh/stuffbin v1.1.0 h1:f5S5BHzZALjuJEgTIOMC9NidEnBJM7Ze6Lu1GHR/lwU=
github.com/knadh/stuffbin v1.1.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
3 changes: 3 additions & 0 deletions i18n/cs-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Opakování",
"settings.smtp.retriesHelp": "Počet opakovaných pokusů, když zpráva selže.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Nastavit vlastní záhlaví",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Nastavení",
"settings.updateAvailable": "Nová aktualizace {version} je k dispozici.",
"subscribers.advancedQuery": "Rozšířené",
Expand Down
3 changes: 3 additions & 0 deletions i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Wiederholungen",
"settings.smtp.retriesHelp": "Maximale Anzahl an Wiederholungen, wenn eine Machricht fehlschlägt.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Benutzerdefinierten Header verwenden",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Einstellungen",
"settings.updateAvailable": "Ein neues Update auf {version} ist verfügbar.",
"subscribers.advancedQuery": "Erweitert",
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Retries",
"settings.smtp.retriesHelp": "Number of times to retry when a message fails.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Set custom headers",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Settings",
"settings.updateAvailable": "A new update {version} is available.",
"subscribers.advancedQuery": "Advanced",
Expand Down
3 changes: 3 additions & 0 deletions i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Reintentos",
"settings.smtp.retriesHelp": "Número de reintentos cuando un mensaje falla.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Configurar encabezados personalizados.",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Configuraciones",
"settings.updateAvailable": "Una actualización {version} está disponible.",
"subscribers.advancedQuery": "Avanzado",
Expand Down
3 changes: 3 additions & 0 deletions i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Retries",
"settings.smtp.retriesHelp": "Number of times to retry when a message fails.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Set custom headers",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Settings",
"settings.updateAvailable": "A new update {version} is available.",
"subscribers.advancedQuery": "Advanced",
Expand Down
3 changes: 3 additions & 0 deletions i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Tentatives de renvoi",
"settings.smtp.retriesHelp": "Nombre de tentatives de renvoi d'un message en cas d'échec",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Définir des en-têtes personnalisés",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Paramètres",
"settings.updateAvailable": "Une nouvelle version ({version}) est disponible.",
"subscribers.advancedQuery": "Requête avancée",
Expand Down
3 changes: 3 additions & 0 deletions i18n/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Újrapróbálkozások",
"settings.smtp.retriesHelp": "Az újrapróbálkozások száma, ha az üzenet sikertelen.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Egyéni fejlécek beállítása",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Beállítások",
"settings.updateAvailable": "Új frissítés {version} elérhető.",
"subscribers.advancedQuery": "További beállítások",
Expand Down
3 changes: 3 additions & 0 deletions i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "Tentativi",
"settings.smtp.retriesHelp": "Numero di tentativi in caso di errore invio messaggio.",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "Definisci intestazioni personalizzate",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "Impostazioni",
"settings.updateAvailable": "È a disposizione una nuova versione {version}.",
"subscribers.advancedQuery": "Avanzate",
Expand Down
3 changes: 3 additions & 0 deletions i18n/jp.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,10 @@
"settings.smtp.name": "SMTP",
"settings.smtp.retries": "再トライ",
"settings.smtp.retriesHelp": "メッセージ送信失敗時の再試行数",
"settings.smtp.sendTest": "Send e-mail",
"settings.smtp.setCustomHeaders": "カスタムヘッダー設定",
"settings.smtp.testConnection": "Test connection",
"settings.smtp.toEmail": "To e-mail",
"settings.title": "設定",
"settings.updateAvailable": "新しい {バージョン} の更新が可能です。",
"subscribers.advancedQuery": "アドバンスド",
Expand Down
Loading

0 comments on commit ee44817

Please sign in to comment.