Skip to content

Commit

Permalink
Merge pull request #13 from wneessen/issue_12
Browse files Browse the repository at this point in the history
#12: add confirmation mail feature
  • Loading branch information
wneessen authored Jan 27, 2022
2 parents 299a1ed + cdd693e commit 885a288
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 42 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -223,12 +236,16 @@ The API response to a send request (`/api/v1/send/<formid>/<token>`) looks like
```json
{
"form_id": "test_form",
"send_time": 1628670331
"send_time": 1628670331,
"confirmation_sent": true,
"confirmation_rcpt": "[email protected]"
}
```

* `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

Expand Down
96 changes: 76 additions & 20 deletions api/sendform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"fmt"
"github.com/wneessen/js-mailer/form"
"github.com/wneessen/js-mailer/response"
"net/http"
"time"
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
40 changes: 23 additions & 17 deletions form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 885a288

Please sign in to comment.