3
0

Add support for SMTP Configurations test

This commit is contained in:
kinyaelgrande
2022-11-17 16:32:37 +03:00
parent ee24191c70
commit 1f1f0d2ce3
16 changed files with 803 additions and 2 deletions

View File

@@ -1,8 +1,8 @@
<template>
<b-card
class="shadow-sm"
header-bg-variant="white"
footer-bg-variant="white"
class="shadow-sm"
>
<b-form
@submit.prevent="submit()"
@@ -34,6 +34,7 @@
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-form-group
:label="$t('user.label')"
:description="$t('user.description')"
@@ -45,6 +46,7 @@
autocomplete="off"
/>
</b-form-group>
<b-form-group
:label="$t('password.label')"
:description="$t('password.description')"
@@ -86,6 +88,7 @@
{{ $t('tlsInsecure.label') }}
</b-form-checkbox>
</b-form-group>
<b-form-group
:label="$t('tlsServerName.label')"
:description="$t('tlsServerName.description')"
@@ -106,10 +109,21 @@
<template #footer>
<c-submit-button
class="float-right"
:disabled="disabled"
:processing="processing"
:success="success"
variant="light"
class="float-left"
@submit="smtpConnectionCheck()"
>
{{ $t('testSmtpConfigs.button') }}
</c-submit-button>
<c-submit-button
:disabled="disabled"
:processing="processing"
:success="success"
class="float-right"
@submit="submit()"
/>
</template>
@@ -172,6 +186,10 @@ export default {
submit () {
this.$emit('submit', this.server)
},
smtpConnectionCheck () {
this.$emit('smtpConnectionCheck', this.server)
},
},
}
</script>

View File

@@ -13,6 +13,7 @@
:success="auth.success"
:disabled="!canManage"
@submit="onEmailServerSubmit($event)"
@smtpConnectionCheck="onSmtpConnectionCheck($event)"
/>
</b-container>
</template>
@@ -113,6 +114,41 @@ export default {
this.external.processing = false
})
},
onSmtpConnectionCheck (server) {
this.external.processing = true
// Append the list of recepient's email Addresses
const recepients = []
recepients.push(server.from)
this.$SystemAPI.smtpConfigurationCheckerCheck({
host: server.host,
port: parseInt(server.port),
recipients: recepients,
username: server.user,
password: server.pass,
tlsInsecure: server.tlsInsecure,
tlsServerName: server.tlsServerName,
})
.then(response => {
if (Object.values(response).every(resp => resp === '')) {
this.animateSuccess('external')
this.toastSuccess(this.$t('notification:settings.system.smtpCheck.success'))
}
Object.keys(response).forEach(key => {
if (response[key]) {
this.toastWarning(`${key}: ${response[key]}`)
}
})
})
.catch(this.toastErrorHandler(this.$t('notification:settings.system.smtpCheck.error')))
.finally(() => {
this.external.processing = false
})
},
},
}
</script>

View File

@@ -4522,4 +4522,39 @@ export default class System {
return '/data-privacy/connection/'
}
// Check SMTP server configuration settings
async smtpConfigurationCheckerCheck (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
const {
host,
port,
recipients,
username,
password,
tlsInsecure,
tlsServerName,
} = (a as KV) || {}
if (!host) {
throw Error('field host is empty')
}
const cfg: AxiosRequestConfig = {
...extra,
method: 'post',
url: this.smtpConfigurationCheckerCheckEndpoint(),
}
cfg.data = {
host,
port,
recipients,
username,
password,
tlsInsecure,
tlsServerName,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
smtpConfigurationCheckerCheckEndpoint (): string {
return '/smtp/configuration-checker/'
}
}

View File

@@ -235,6 +235,9 @@ settings:
style:
success: Auth background styles updated
error: Auth background styles update failed
smtpCheck:
success: SMTP configuration check passed
error: SMTP configuration check failed
compose:
fetch:
error: Compose settings fetch failed

View File

@@ -27,3 +27,6 @@ editor:
tlsServerName:
label: TLS Server name
description: Optional, If SMTP server uses a different value than used for the server name.
testSmtpConfigs:
button: Test SMTP Server

56
server/pkg/mail/check.go Normal file
View File

@@ -0,0 +1,56 @@
package mail
import (
"crypto/tls"
"fmt"
"net"
"net/smtp"
"regexp"
"time"
)
const (
hostCheckRE = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"
)
// ConfigCheck dials and authenticates to an SMTP server.
func ConfigCheck(host string, port uint, username string, password string, tlsConfig *tls.Config) (checkRes string) {
addr := fmt.Sprintf("%s:%d", host, port)
// Dial the tcp connection
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return err.Error()
}
conn.SetDeadline(time.Now().Add(10 * time.Second))
c, err := smtp.NewClient(conn, host)
if err != nil {
return fmt.Sprintf("SMTP connection to %s timeout!", addr)
}
defer c.Close()
// Check whether STARTTLS extension is supported
ok, _ := c.Extension("STARTTLS")
if ok {
err = c.StartTLS(tlsConfig)
if err != nil {
return err.Error()
}
}
// Authentication
auth := smtp.PlainAuth("", username, password, host)
err = c.Auth(auth)
if err != nil {
return err.Error()
}
return
}
func IsValidHost(host string) bool {
hostCheck := regexp.MustCompile(hostCheckRE)
return hostCheck.MatchString(host)
}

View File

@@ -0,0 +1,30 @@
package mail
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestHostValidator(t *testing.T) {
ttc := []struct {
host string
ok bool
}{
{"ç$€§az.com", false},
{"@sendyy.com", false},
{"qwertyuiop.com", true},
{"test.foo.bar", true},
{"10.10.10", true},
{"192.10.10345", true},
{"1rg.10ui.10", true},
{"info .crust tech", false},
{"info.crust.tech", true},
{"crust-tech?", false},
{"crust-tech", true},
{"crust/tech", false},
}
for _, tc := range ttc {
require.True(t, IsValidHost(tc.host) == tc.ok, "Validation of %s should return %v", tc.host, tc.ok)
}
}

View File

@@ -0,0 +1,17 @@
templates:
smtp_configuration_check_subject:
type: text/plain
meta:
short: SMTP configuration check content
template: SMTP connection check
smtp_configuration_check_content:
type: text/html
meta:
short: SMTP configuration check content
template: |-
{{template "email_general_header" .}}
<h2 style="color: #568ba2;text-align: center;">SMTP configurations check</h2>
<p>Hello,</p>
<p>Your SMTP configuration test passed</p>
{{template "email_general_footer" .}}

View File

@@ -2256,3 +2256,21 @@ endpoints:
required: false
title: Exclude (0, default), include (1) or return only (2) deleted connections
type: filter.State
- title: SMTP Configuration Checker
entrypoint: smtpConfigurationChecker
path: "/smtp"
apis:
- name: check
method: POST
title: Check SMTP server configuration settings
path: "/configuration-checker/"
parameters:
post:
- { name: host, type: string, title: SMTP server host name, required: true }
- { name: port, type: uint, title: SMTP server port, }
- { name: recipients, type: "[]string", title: List of recipients email addresses that should recieve test email }
- { name: username, type: string, title: SMTP server authentication username }
- { name: password, type: string, title: SMTP server authentication password }
- { name: tlsInsecure, type: bool, title: TLS mode }
- { name: tlsServerName, type: string, title: TLS server name }

View File

@@ -0,0 +1,57 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
SmtpConfigurationCheckerAPI interface {
Check(context.Context, *request.SmtpConfigurationCheckerCheck) (interface{}, error)
}
// HTTP API interface
SmtpConfigurationChecker struct {
Check func(http.ResponseWriter, *http.Request)
}
)
func NewSmtpConfigurationChecker(h SmtpConfigurationCheckerAPI) *SmtpConfigurationChecker {
return &SmtpConfigurationChecker{
Check: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSmtpConfigurationCheckerCheck()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Check(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h SmtpConfigurationChecker) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Post("/smtp/configuration-checker/", h.Check)
})
}

View File

@@ -0,0 +1,250 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
SmtpConfigurationCheckerCheck struct {
// Host POST parameter
//
// SMTP server host name
Host string
// Port POST parameter
//
// SMTP server port
Port uint
// Recipients POST parameter
//
// List of recipients email addresses that should recieve test email
Recipients []string
// Username POST parameter
//
// SMTP server authentication username
Username string
// Password POST parameter
//
// SMTP server authentication password
Password string
// TlsInsecure POST parameter
//
// TLS mode
TlsInsecure bool
// TlsServerName POST parameter
//
// TLS server name
TlsServerName string
}
)
// NewSmtpConfigurationCheckerCheck request
func NewSmtpConfigurationCheckerCheck() *SmtpConfigurationCheckerCheck {
return &SmtpConfigurationCheckerCheck{}
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) Auditable() map[string]interface{} {
return map[string]interface{}{
"host": r.Host,
"port": r.Port,
"recipients": r.Recipients,
"username": r.Username,
"password": r.Password,
"tlsInsecure": r.TlsInsecure,
"tlsServerName": r.TlsServerName,
}
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetHost() string {
return r.Host
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetPort() uint {
return r.Port
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetRecipients() []string {
return r.Recipients
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetUsername() string {
return r.Username
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetPassword() string {
return r.Password
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetTlsInsecure() bool {
return r.TlsInsecure
}
// Auditable returns all auditable/loggable parameters
func (r SmtpConfigurationCheckerCheck) GetTlsServerName() string {
return r.TlsServerName
}
// Fill processes request and fills internal variables
func (r *SmtpConfigurationCheckerCheck) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["host"]; ok && len(val) > 0 {
r.Host, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["port"]; ok && len(val) > 0 {
r.Port, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["username"]; ok && len(val) > 0 {
r.Username, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["password"]; ok && len(val) > 0 {
r.Password, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["tlsInsecure"]; ok && len(val) > 0 {
r.TlsInsecure, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["tlsServerName"]; ok && len(val) > 0 {
r.TlsServerName, err = val[0], nil
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["host"]; ok && len(val) > 0 {
r.Host, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["port"]; ok && len(val) > 0 {
r.Port, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
//if val, ok := req.Form["recipients[]"]; ok && len(val) > 0 {
// r.Recipients, err = val, nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["username"]; ok && len(val) > 0 {
r.Username, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["password"]; ok && len(val) > 0 {
r.Password, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["tlsInsecure"]; ok && len(val) > 0 {
r.TlsInsecure, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["tlsServerName"]; ok && len(val) > 0 {
r.TlsServerName, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}

View File

@@ -51,6 +51,7 @@ func MountRoutes() func(r chi.Router) {
// @todo move these two to dataPrivacy routes
handlers.NewDataPrivacyRequest(DataPrivacyRequest{}.New()).MountRoutes(r)
handlers.NewDataPrivacyRequestComment(DataPrivacyRequestComment{}.New()).MountRoutes(r)
handlers.NewSmtpConfigurationChecker(SmtpConfigurationChecker{}.New()).MountRoutes(r)
})
}
}

View File

@@ -0,0 +1,47 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
SmtpConfigurationChecker struct {
svc smtpConfigurationCheckerService
}
smtpConfigurationCheckerService interface {
Check(context.Context, *types.SmtpConfiguration) (*types.SmtpCheckResult, error)
}
)
func (SmtpConfigurationChecker) New() *SmtpConfigurationChecker {
return &SmtpConfigurationChecker{
svc: service.DefaultSMTPChecker,
}
}
func (ctrl *SmtpConfigurationChecker) Check(ctx context.Context, r *request.SmtpConfigurationCheckerCheck) (interface{}, error) {
var (
err error
checkResults = &types.SmtpCheckResult{}
smtp = &types.SmtpConfiguration{
Host: r.Host,
Port: r.Port,
Recipients: r.Recipients,
Username: r.Username,
Password: r.Password,
TLSInsecure: r.TlsInsecure,
TLSServerName: r.TlsServerName,
}
)
checkResults, err = ctrl.svc.Check(ctx, smtp)
if err != nil {
return nil, err
}
return checkResults, nil
}

View File

@@ -91,6 +91,7 @@ var (
DefaultApigwProfiler *apigwProfiler
DefaultReport *report
DefaultDataPrivacy *dataPrivacy
DefaultSMTPChecker *smtpConfigurationChecker
DefaultStatistics *statistics
@@ -216,6 +217,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
DefaultApigwProfiler = Profiler()
DefaultApigwFilter = Filter()
DefaultDataPrivacy = DataPrivacy(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service())
DefaultSMTPChecker = SmtpConfigurationChecker(CurrentSettings, DefaultRenderer, DefaultAccessControl, c.Auth)
if err = initRoles(ctx, log.Named("rbac.roles"), c.RBAC, eventbus.Service(), rbac.Global()); err != nil {
return err

View File

@@ -0,0 +1,207 @@
package service
import (
"context"
"crypto/tls"
"fmt"
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/mail"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/types"
gomail "gopkg.in/mail.v2"
htpl "html/template"
"io/ioutil"
"strings"
)
type (
smtpConfigurationChecker struct {
settings *types.AppSettings
ts TemplateService
opt options.AuthOpt
accessControl smtpCheckAccessController
}
smtpCheckAccessController interface {
CanManageSettings(context.Context) bool
}
)
func SmtpConfigurationChecker(s *types.AppSettings, ts TemplateService, ac accessController, opt options.AuthOpt) *smtpConfigurationChecker {
return &smtpConfigurationChecker{
settings: s,
ts: ts,
opt: opt,
accessControl: ac,
}
}
// Check SMTP server configurations and send a test email
// to recipients if they're provided
func (svc smtpConfigurationChecker) Check(ctx context.Context, smtpConfigs *types.SmtpConfiguration) (checkResults *types.SmtpCheckResult, err error) {
if !svc.accessControl.CanManageSettings(ctx) {
return nil, fmt.Errorf("not allowed to check SMTP configurations")
}
var (
tlsConfig = &tls.Config{}
)
checkResults = &types.SmtpCheckResult{}
if smtpConfigs.Port == 0 {
smtpConfigs.Port = 25
}
//check for validity of the host
if !mail.IsValidHost(smtpConfigs.Host) {
checkResults.Host = fmt.Sprintf("%s name is Invalid", smtpConfigs.Host)
}
// Applying TLS
tlsConfig = &tls.Config{ServerName: smtpConfigs.Host}
if smtpConfigs.TLSInsecure {
tlsConfig.InsecureSkipVerify = true
}
if smtpConfigs.TLSServerName != "" {
tlsConfig.ServerName = smtpConfigs.TLSServerName
}
checkResults.Server = mail.ConfigCheck(smtpConfigs.Host, smtpConfigs.Port, smtpConfigs.Username, smtpConfigs.Password, tlsConfig)
//send the email there are recipients
if checkResults.Server == "" {
if len(smtpConfigs.Recipients) != 0 {
checkResults.Send, err = svc.smtpSend(ctx, smtpConfigs.Recipients)
}
}
return checkResults, err
}
func (svc smtpConfigurationChecker) smtpSend(ctx context.Context, recipients []string) (expected string, err error) {
var (
ntf = mail.New()
toHeader string
// context with service user
// we need this for retrieving & rendering email templates
suCtx = intAuth.SetIdentityToContext(ctx, intAuth.ServiceUser())
)
if err = svc.procEmailRecipients(ntf, "To", recipients); err != nil {
return "", err
}
toHeader = strings.Join(recipients, ",")
ntf.SetAddressHeader("To", toHeader, "")
st, ct, err := svc.findEmailTemplates(suCtx)
// if we cannot find an email template
if err != nil {
ntf.SetHeader("Subject", "SMTP Configuration check")
ntf.SetBody("text/html", "<h2 style=\"color: #568ba2;text-align: center;\">SMTP configurations check passed</h2>")
err = mail.Send(ntf)
if err != nil {
return err.Error(), nil
}
return "", nil
}
subjectTmp, contentTmp, err := svc.procEmailTemplate(suCtx, st.ID, ct.ID)
if err != nil {
return "", err
}
ntf.SetHeader("Subject", string(subjectTmp))
ntf.SetBody("text/html", string(contentTmp))
err = mail.Send(ntf)
if err != nil {
return err.Error(), nil
}
return "", nil
}
func (svc smtpConfigurationChecker) procEmailRecipients(m *gomail.Message, field string, recipients []string) (err error) {
var (
email string
rcpt string
)
if len(recipients) == 0 {
return
}
for _, rcpt = range recipients {
email = strings.TrimSpace(rcpt)
// Validate email here
if !mail.IsValidAddress(email) {
return fmt.Errorf("invalid recipient email address %s", email)
}
}
m.SetHeader(field, recipients...)
return nil
}
// procEmailTemplate processes Email address template based on the template's subject ID and content ID
func (svc smtpConfigurationChecker) procEmailTemplate(ctx context.Context, stId uint64, ctId uint64) (subjectTmp []byte, contentTmp []byte, err error) {
// Prepare payload
payload := map[string]interface{}{
"Logo": htpl.URL(svc.settings.General.Mail.Logo),
"BaseURL": svc.opt.BaseURL,
}
// Render document
subject, err := svc.ts.Render(ctx, stId, "text/plain", payload, nil)
if err != nil {
return nil, nil, err
}
content, err := svc.ts.Render(ctx, ctId, "text/plain", payload, nil)
if err != nil {
return nil, nil, err
}
subjectTmp, err = ioutil.ReadAll(subject)
if err != nil {
return nil, nil, err
}
contentTmp, err = ioutil.ReadAll(content)
if err != nil {
return nil, nil, err
}
return subjectTmp, contentTmp, nil
}
func (svc smtpConfigurationChecker) findEmailTemplates(ctx context.Context) (st *types.Template, ct *types.Template, err error) {
var (
hdl string
)
hdl = "smtp_configuration_check_subject"
st, err = svc.ts.FindByHandle(ctx, hdl)
if err != nil {
return nil, nil, err
}
hdl = "smtp_configuration_check_content"
ct, err = svc.ts.FindByHandle(ctx, hdl)
if err != nil {
return nil, nil, err
}
return st, ct, nil
}

View File

@@ -0,0 +1,21 @@
package types
type (
SmtpConfiguration struct {
Host string
Port uint
Recipients []string
Username string
Password string
TLSInsecure bool
TLSServerName string
}
// SmtpCheckResult represents the messages returned after SMTP Host validation,
// SMTP Server configurations check and Send test email process
SmtpCheckResult struct {
Host string `json:"host"`
Server string `json:"server"`
Send string `json:"send"`
}
)