Skip to content

Commit 885a288

Browse files
authored
Merge pull request #13 from wneessen/issue_12
#12: add confirmation mail feature
2 parents 299a1ed + cdd693e commit 885a288

File tree

6 files changed

+121
-42
lines changed

6 files changed

+121
-42
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.
1919
* hCaptcha support
2020
* reCaptcha v2 support
2121
* Form field type validation (text, email, number, bool)
22+
* Confirmation mail to poster
2223

2324
### Planed features
2425

@@ -94,6 +95,12 @@ the JSON syntax of the form configuration is very simple, yet flexible.
9495
"message"
9596
]
9697
},
98+
"confirmation": {
99+
"enabled": true,
100+
"rcpt_field": "email",
101+
"subject": "Thank you for your message",
102+
"content": "We have received your message via www.example.com and will tough base with you, shortly."
103+
},
97104
"validation": {
98105
"hcaptcha": {
99106
"enabled": true,
@@ -141,6 +148,12 @@ the JSON syntax of the form configuration is very simple, yet flexible.
141148
* `content (type: struct)`: The struct for the mail content configuration
142149
* `subject (type: string)`: Subject for the mail notification of the form submission
143150
* `fields (type: []string)`: List of field names that should show up in the mail notification
151+
* `confirmation (type: struct)`: The struct for the mail confirmail mail configuration
152+
* `enabled (type: boolean)`: If true, the confirmation mail will be sent
153+
* `rcpt_field (type: string)`: Name of the form field holding the confirmation mail recipient
154+
* `subject (type: string)`: Subject for the confirmation mail
155+
* `content (type: string)`: Content for the confirmation mail
156+
* `fields (type: []string)`: List of field names that should show up in the mail notification
144157
* `validation (type: struct)`: The struct for the form validation configuration
145158
* `hcaptcha (type: struct)`: The struct for the forms hCaptcha configuration
146159
* `enabled (type: bool)`: Enable hCaptcha challenge-response validation
@@ -223,12 +236,16 @@ The API response to a send request (`/api/v1/send/<formid>/<token>`) looks like
223236
```json
224237
{
225238
"form_id": "test_form",
226-
"send_time": 1628670331
239+
"send_time": 1628670331,
240+
"confirmation_sent": true,
241+
"confirmation_rcpt": "toni.tester@example.com"
227242
}
228243
```
229244

230245
* `form_id (type: string)`: The form id of the current form (for reference)
231246
* `send_time (type: int64)`: The epoch timestamp when the message was sent
247+
* `confirmation_sent (type: boolean)`: Is set to true, if a confirmation was sent successfully
248+
* `confirmation_rcpt (type: string)`: The recipient mail address that the confirmation was sent to
232249

233250
### Error response
234251

api/sendform.go

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"fmt"
5+
"github.com/wneessen/js-mailer/form"
56
"github.com/wneessen/js-mailer/response"
67
"net/http"
78
"time"
@@ -10,10 +11,12 @@ import (
1011
"github.com/labstack/echo/v4"
1112
)
1213

13-
// SentSuccessfull represents confirmation JSON structure for a successfully sent message
14-
type SentSuccessfull struct {
15-
FormId string `json:"form_id"`
16-
SendTime int64 `json:"send_time"`
14+
// SentSuccessful represents confirmation JSON structure for a successfully sent message
15+
type SentSuccessful struct {
16+
FormId string `json:"form_id"`
17+
SendTime int64 `json:"send_time"`
18+
ConfirmationSent bool `json:"confirmation_sent"`
19+
ConfirmationRcpt string `json:"confirmation_rcpt"`
1720
}
1821

1922
// SendForm handles the HTTP form sending API request
@@ -24,6 +27,33 @@ func (r *Route) SendForm(c echo.Context) error {
2427
return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
2528
}
2629

30+
// Do we have some confirmation mail to handle?
31+
confirmWasSent := false
32+
confirmRcpt := ""
33+
if sr.FormObj.Confirmation.Enabled {
34+
sendConfirm := true
35+
confirmRcpt = c.FormValue(sr.FormObj.Confirmation.RecipientField)
36+
if confirmRcpt == "" {
37+
c.Logger().Warnf("confirmation mail feature activated, but recpienent field not found or empty")
38+
sendConfirm = false
39+
}
40+
if sr.FormObj.Confirmation.Subject == "" {
41+
c.Logger().Warnf("confirmation mail feature activated, but no subject found in configuration")
42+
sendConfirm = false
43+
}
44+
if sr.FormObj.Confirmation.Content == "" {
45+
c.Logger().Warnf("confirmation mail feature activated, but no content found in configuration")
46+
sendConfirm = false
47+
}
48+
if sendConfirm {
49+
confirmWasSent = true
50+
if err := SendFormConfirmation(sr.FormObj, confirmRcpt); err != nil {
51+
c.Logger().Warnf("failed to send confirmation mail: %s", err)
52+
confirmWasSent = false
53+
}
54+
}
55+
}
56+
2757
// Compose the mail message
2858
mailMsg := mail.NewMessage()
2959
mailMsg.SetHeader("From", sr.FormObj.Sender)
@@ -39,19 +69,7 @@ func (r *Route) SendForm(c echo.Context) error {
3969
mailMsg.SetBody("text/plain", mailBody)
4070

4171
// Send the mail message
42-
var serverTimeout time.Duration
43-
var err error
44-
serverTimeout, err = time.ParseDuration(sr.FormObj.Server.Timeout)
45-
if err != nil {
46-
c.Logger().Warnf("Could not parse configured server timeout: %s", err)
47-
serverTimeout = time.Second * 5
48-
}
49-
mailDailer := mail.NewDialer(sr.FormObj.Server.Host, sr.FormObj.Server.Port, sr.FormObj.Server.Username,
50-
sr.FormObj.Server.Password)
51-
mailDailer.Timeout = serverTimeout
52-
if sr.FormObj.Server.ForceTLS {
53-
mailDailer.StartTLSPolicy = mail.MandatoryStartTLS
54-
}
72+
mailDailer := GetMailDailer(sr.FormObj)
5573
mailSender, err := mailDailer.Dial()
5674
if err != nil {
5775
c.Logger().Errorf("Could not connect to configured mail server: %s", err)
@@ -76,9 +94,47 @@ func (r *Route) SendForm(c echo.Context) error {
7694
return c.JSON(http.StatusOK, response.SuccessResponse{
7795
StatusCode: http.StatusOK,
7896
Status: http.StatusText(http.StatusOK),
79-
Data: &SentSuccessfull{
80-
FormId: sr.FormObj.Id,
81-
SendTime: time.Now().Unix(),
97+
Data: SentSuccessful{
98+
FormId: sr.FormObj.Id,
99+
SendTime: time.Now().Unix(),
100+
ConfirmationSent: confirmWasSent,
101+
ConfirmationRcpt: confirmRcpt,
82102
},
83103
})
84104
}
105+
106+
// SendFormConfirmation sends out a confirmation mail if requested in the form
107+
func SendFormConfirmation(f *form.Form, r string) error {
108+
mailMsg := mail.NewMessage()
109+
mailMsg.SetHeader("From", f.Sender)
110+
mailMsg.SetHeader("To", r)
111+
mailMsg.SetHeader("Subject", f.Confirmation.Subject)
112+
mailMsg.SetBody("text/plain", f.Confirmation.Content)
113+
mailDailer := GetMailDailer(f)
114+
mailSender, err := mailDailer.Dial()
115+
if err != nil {
116+
return fmt.Errorf("could not connect to configured mail server: %w", err)
117+
}
118+
if err := mail.Send(mailSender, mailMsg); err != nil {
119+
return fmt.Errorf("could not send confirmation mail message: %w", err)
120+
}
121+
if err := mailSender.Close(); err != nil {
122+
return fmt.Errorf("failed to close mail server connection: %w", err)
123+
}
124+
return nil
125+
}
126+
127+
// GetMailDailer returns a new mail dailer object based on the form configuration
128+
func GetMailDailer(f *form.Form) *mail.Dialer {
129+
var serverTimeout time.Duration
130+
serverTimeout, err := time.ParseDuration(f.Server.Timeout)
131+
if err != nil {
132+
serverTimeout = time.Second * 5
133+
}
134+
mailDailer := mail.NewDialer(f.Server.Host, f.Server.Port, f.Server.Username, f.Server.Password)
135+
mailDailer.Timeout = serverTimeout
136+
if f.Server.ForceTLS {
137+
mailDailer.StartTLSPolicy = mail.MandatoryStartTLS
138+
}
139+
return mailDailer
140+
}

form/form.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,49 @@ import (
1010

1111
// Form reflect the configuration struct for form configurations
1212
type Form struct {
13+
Content struct {
14+
Subject string
15+
Fields []string
16+
}
17+
Confirmation struct {
18+
Enabled bool `fig:"enabled"`
19+
RecipientField string `fig:"rcpt_field" validate:"required"`
20+
Subject string `fig:"subject" validate:"required"`
21+
Content string `fig:"content" validate:"required"`
22+
}
23+
Domains []string `fig:"domains" validate:"required"`
1324
Id string `fig:"id" validate:"required"`
14-
Secret string `fig:"secret" validate:"required"`
1525
Recipients []string `fig:"recipients" validate:"required"`
26+
Secret string `fig:"secret" validate:"required"`
1627
Sender string `fig:"sender" validate:"required"`
17-
Domains []string `fig:"domains" validate:"required"`
28+
Server struct {
29+
Host string `fig:"host" validate:"required"`
30+
Port int `fig:"port" default:"25"`
31+
Username string
32+
Password string
33+
Timeout string `fig:"timeout" default:"5s"`
34+
ForceTLS bool `fig:"force_tls"`
35+
}
1836
Validation struct {
37+
Fields []ValidationField `fig:"fields"`
1938
Hcaptcha struct {
2039
Enabled bool `fig:"enabled"`
2140
SecretKey string `fig:"secret_key"`
2241
}
42+
Honeypot *string `fig:"honeypot"`
2343
Recaptcha struct {
2444
Enabled bool `fig:"enabled"`
2545
SecretKey string `fig:"secret_key"`
2646
}
27-
Fields []ValidationField `fig:"fields"`
28-
Honeypot *string `fig:"honeypot"`
29-
}
30-
Content struct {
31-
Subject string
32-
Fields []string
33-
}
34-
Server struct {
35-
Host string `fig:"host" validate:"required"`
36-
Port int `fig:"port" default:"25"`
37-
Username string
38-
Password string
39-
Timeout string `fig:"timeout" default:"5s"`
40-
ForceTLS bool `fig:"force_tls"`
4147
}
4248
}
4349

4450
// ValidationField reflects the struct for a form validation field
4551
type ValidationField struct {
4652
Name string `fig:"name" validate:"required"`
53+
Required bool `fig:"required"`
4754
Type string `fig:"type"`
4855
Value string `fig:"value"`
49-
Required bool `fig:"required"`
5056
}
5157

5258
// NewForm returns a new Form object to the caller. It fails with an error when

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/wneessen/js-mailer
33
go 1.16
44

55
require (
6-
github.com/ReneKroon/ttlcache/v2 v2.7.0
6+
github.com/ReneKroon/ttlcache/v2 v2.11.0
77
github.com/cyphar/filepath-securejoin v0.2.3
88
github.com/go-mail/mail v2.3.1+incompatible
99
github.com/kkyr/fig v0.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/ReneKroon/ttlcache/v2 v2.7.0 h1:sZeaSwA2UN/y/h7CvkW15Kovd2Oiy76CBDORiOwHPwI=
2-
github.com/ReneKroon/ttlcache/v2 v2.7.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
1+
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
2+
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
33
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
44
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
55
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
)
1818

1919
// VERSION is the global version string contstant
20-
const VERSION = "0.2.1"
20+
const VERSION = "0.2.2"
2121

2222
// Srv represents the server object
2323
type Srv struct {

0 commit comments

Comments
 (0)