From cdd693eac976f2a8666b9bf5763b9a12d3460861 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 27 Jan 2022 12:28:10 +0100 Subject: [PATCH] #12: add confirmation mail feature One can now configure a "confirmation" via form config. This allows the form holder to send a mail to the "poster" to confirm the receipt --- README.md | 19 +++++++++- api/sendform.go | 96 ++++++++++++++++++++++++++++++++++++++---------- form/form.go | 40 +++++++++++--------- go.mod | 2 +- go.sum | 4 +- server/server.go | 2 +- 6 files changed, 121 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index df7adfe..7c60193 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`. * hCaptcha support * reCaptcha v2 support * Form field type validation (text, email, number, bool) +* Confirmation mail to poster ### Planed features @@ -94,6 +95,12 @@ the JSON syntax of the form configuration is very simple, yet flexible. "message" ] }, + "confirmation": { + "enabled": true, + "rcpt_field": "email", + "subject": "Thank you for your message", + "content": "We have received your message via www.example.com and will tough base with you, shortly." + }, "validation": { "hcaptcha": { "enabled": true, @@ -141,6 +148,12 @@ the JSON syntax of the form configuration is very simple, yet flexible. * `content (type: struct)`: The struct for the mail content configuration * `subject (type: string)`: Subject for the mail notification of the form submission * `fields (type: []string)`: List of field names that should show up in the mail notification +* `confirmation (type: struct)`: The struct for the mail confirmail mail configuration + * `enabled (type: boolean)`: If true, the confirmation mail will be sent + * `rcpt_field (type: string)`: Name of the form field holding the confirmation mail recipient + * `subject (type: string)`: Subject for the confirmation mail + * `content (type: string)`: Content for the confirmation mail + * `fields (type: []string)`: List of field names that should show up in the mail notification * `validation (type: struct)`: The struct for the form validation configuration * `hcaptcha (type: struct)`: The struct for the forms hCaptcha configuration * `enabled (type: bool)`: Enable hCaptcha challenge-response validation @@ -223,12 +236,16 @@ The API response to a send request (`/api/v1/send//`) looks like ```json { "form_id": "test_form", - "send_time": 1628670331 + "send_time": 1628670331, + "confirmation_sent": true, + "confirmation_rcpt": "toni.tester@example.com" } ``` * `form_id (type: string)`: The form id of the current form (for reference) * `send_time (type: int64)`: The epoch timestamp when the message was sent +* `confirmation_sent (type: boolean)`: Is set to true, if a confirmation was sent successfully +* `confirmation_rcpt (type: string)`: The recipient mail address that the confirmation was sent to ### Error response diff --git a/api/sendform.go b/api/sendform.go index 06e83fd..346d644 100644 --- a/api/sendform.go +++ b/api/sendform.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/wneessen/js-mailer/form" "github.com/wneessen/js-mailer/response" "net/http" "time" @@ -10,10 +11,12 @@ import ( "github.com/labstack/echo/v4" ) -// SentSuccessfull represents confirmation JSON structure for a successfully sent message -type SentSuccessfull struct { - FormId string `json:"form_id"` - SendTime int64 `json:"send_time"` +// SentSuccessful represents confirmation JSON structure for a successfully sent message +type SentSuccessful struct { + FormId string `json:"form_id"` + SendTime int64 `json:"send_time"` + ConfirmationSent bool `json:"confirmation_sent"` + ConfirmationRcpt string `json:"confirmation_rcpt"` } // SendForm handles the HTTP form sending API request @@ -24,6 +27,33 @@ func (r *Route) SendForm(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") } + // Do we have some confirmation mail to handle? + confirmWasSent := false + confirmRcpt := "" + if sr.FormObj.Confirmation.Enabled { + sendConfirm := true + confirmRcpt = c.FormValue(sr.FormObj.Confirmation.RecipientField) + if confirmRcpt == "" { + c.Logger().Warnf("confirmation mail feature activated, but recpienent field not found or empty") + sendConfirm = false + } + if sr.FormObj.Confirmation.Subject == "" { + c.Logger().Warnf("confirmation mail feature activated, but no subject found in configuration") + sendConfirm = false + } + if sr.FormObj.Confirmation.Content == "" { + c.Logger().Warnf("confirmation mail feature activated, but no content found in configuration") + sendConfirm = false + } + if sendConfirm { + confirmWasSent = true + if err := SendFormConfirmation(sr.FormObj, confirmRcpt); err != nil { + c.Logger().Warnf("failed to send confirmation mail: %s", err) + confirmWasSent = false + } + } + } + // Compose the mail message mailMsg := mail.NewMessage() mailMsg.SetHeader("From", sr.FormObj.Sender) @@ -39,19 +69,7 @@ func (r *Route) SendForm(c echo.Context) error { mailMsg.SetBody("text/plain", mailBody) // Send the mail message - var serverTimeout time.Duration - var err error - serverTimeout, err = time.ParseDuration(sr.FormObj.Server.Timeout) - if err != nil { - c.Logger().Warnf("Could not parse configured server timeout: %s", err) - serverTimeout = time.Second * 5 - } - mailDailer := mail.NewDialer(sr.FormObj.Server.Host, sr.FormObj.Server.Port, sr.FormObj.Server.Username, - sr.FormObj.Server.Password) - mailDailer.Timeout = serverTimeout - if sr.FormObj.Server.ForceTLS { - mailDailer.StartTLSPolicy = mail.MandatoryStartTLS - } + mailDailer := GetMailDailer(sr.FormObj) mailSender, err := mailDailer.Dial() if err != nil { c.Logger().Errorf("Could not connect to configured mail server: %s", err) @@ -76,9 +94,47 @@ func (r *Route) SendForm(c echo.Context) error { return c.JSON(http.StatusOK, response.SuccessResponse{ StatusCode: http.StatusOK, Status: http.StatusText(http.StatusOK), - Data: &SentSuccessfull{ - FormId: sr.FormObj.Id, - SendTime: time.Now().Unix(), + Data: SentSuccessful{ + FormId: sr.FormObj.Id, + SendTime: time.Now().Unix(), + ConfirmationSent: confirmWasSent, + ConfirmationRcpt: confirmRcpt, }, }) } + +// SendFormConfirmation sends out a confirmation mail if requested in the form +func SendFormConfirmation(f *form.Form, r string) error { + mailMsg := mail.NewMessage() + mailMsg.SetHeader("From", f.Sender) + mailMsg.SetHeader("To", r) + mailMsg.SetHeader("Subject", f.Confirmation.Subject) + mailMsg.SetBody("text/plain", f.Confirmation.Content) + mailDailer := GetMailDailer(f) + mailSender, err := mailDailer.Dial() + if err != nil { + return fmt.Errorf("could not connect to configured mail server: %w", err) + } + if err := mail.Send(mailSender, mailMsg); err != nil { + return fmt.Errorf("could not send confirmation mail message: %w", err) + } + if err := mailSender.Close(); err != nil { + return fmt.Errorf("failed to close mail server connection: %w", err) + } + return nil +} + +// GetMailDailer returns a new mail dailer object based on the form configuration +func GetMailDailer(f *form.Form) *mail.Dialer { + var serverTimeout time.Duration + serverTimeout, err := time.ParseDuration(f.Server.Timeout) + if err != nil { + serverTimeout = time.Second * 5 + } + mailDailer := mail.NewDialer(f.Server.Host, f.Server.Port, f.Server.Username, f.Server.Password) + mailDailer.Timeout = serverTimeout + if f.Server.ForceTLS { + mailDailer.StartTLSPolicy = mail.MandatoryStartTLS + } + return mailDailer +} diff --git a/form/form.go b/form/form.go index a884d30..3e0d147 100644 --- a/form/form.go +++ b/form/form.go @@ -10,43 +10,49 @@ import ( // Form reflect the configuration struct for form configurations type Form struct { + Content struct { + Subject string + Fields []string + } + Confirmation struct { + Enabled bool `fig:"enabled"` + RecipientField string `fig:"rcpt_field" validate:"required"` + Subject string `fig:"subject" validate:"required"` + Content string `fig:"content" validate:"required"` + } + Domains []string `fig:"domains" validate:"required"` Id string `fig:"id" validate:"required"` - Secret string `fig:"secret" validate:"required"` Recipients []string `fig:"recipients" validate:"required"` + Secret string `fig:"secret" validate:"required"` Sender string `fig:"sender" validate:"required"` - Domains []string `fig:"domains" validate:"required"` + Server struct { + Host string `fig:"host" validate:"required"` + Port int `fig:"port" default:"25"` + Username string + Password string + Timeout string `fig:"timeout" default:"5s"` + ForceTLS bool `fig:"force_tls"` + } Validation struct { + Fields []ValidationField `fig:"fields"` Hcaptcha struct { Enabled bool `fig:"enabled"` SecretKey string `fig:"secret_key"` } + Honeypot *string `fig:"honeypot"` Recaptcha struct { Enabled bool `fig:"enabled"` SecretKey string `fig:"secret_key"` } - Fields []ValidationField `fig:"fields"` - Honeypot *string `fig:"honeypot"` - } - Content struct { - Subject string - Fields []string - } - Server struct { - Host string `fig:"host" validate:"required"` - Port int `fig:"port" default:"25"` - Username string - Password string - Timeout string `fig:"timeout" default:"5s"` - ForceTLS bool `fig:"force_tls"` } } // ValidationField reflects the struct for a form validation field type ValidationField struct { Name string `fig:"name" validate:"required"` + Required bool `fig:"required"` Type string `fig:"type"` Value string `fig:"value"` - Required bool `fig:"required"` } // NewForm returns a new Form object to the caller. It fails with an error when diff --git a/go.mod b/go.mod index 283e734..b39657b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/wneessen/js-mailer go 1.16 require ( - github.com/ReneKroon/ttlcache/v2 v2.7.0 + github.com/ReneKroon/ttlcache/v2 v2.11.0 github.com/cyphar/filepath-securejoin v0.2.3 github.com/go-mail/mail v2.3.1+incompatible github.com/kkyr/fig v0.3.0 diff --git a/go.sum b/go.sum index f467d48..2913591 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/ReneKroon/ttlcache/v2 v2.7.0 h1:sZeaSwA2UN/y/h7CvkW15Kovd2Oiy76CBDORiOwHPwI= -github.com/ReneKroon/ttlcache/v2 v2.7.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY= +github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM= +github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/server/server.go b/server/server.go index 29cb7ae..3e32d09 100644 --- a/server/server.go +++ b/server/server.go @@ -17,7 +17,7 @@ import ( ) // VERSION is the global version string contstant -const VERSION = "0.2.1" +const VERSION = "0.2.2" // Srv represents the server object type Srv struct {