Initial version
This commit is contained in:
commit
197817cb03
|
@ -0,0 +1 @@
|
||||||
|
/.idea
|
|
@ -0,0 +1,48 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Port int `env:"PORT" envDefault:"8080"`
|
||||||
|
HoneyPots []string `env:"HONEYPOTS" envDefault:"_t_email" envSeparator:","`
|
||||||
|
DefaultRecipient string `env:"DEFAULT_TO"`
|
||||||
|
AllowedRecipients []string `env:"ALLOWED_TO" envSeparator:","`
|
||||||
|
Sender string `env:"EMAIL_FROM"`
|
||||||
|
SmtpUser string `env:"SMTP_USER"`
|
||||||
|
SmtpPassword string `env:"SMTP_PASS"`
|
||||||
|
SmtpHost string `env:"SMTP_HOST"`
|
||||||
|
SmtpPort int `env:"SMTP_PORT" envDefault:"587"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig() (config, error) {
|
||||||
|
cfg := config{}
|
||||||
|
if err := env.Parse(&cfg); err != nil {
|
||||||
|
return cfg, errors.New("failed to parse config")
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRequiredConfig(cfg config) bool {
|
||||||
|
if cfg.DefaultRecipient == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(cfg.AllowedRecipients) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.Sender == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.SmtpUser == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.SmtpPassword == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.SmtpHost == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseConfig(t *testing.T) {
|
||||||
|
t.Run("Default config", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
cfg, err := parseConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.Port != 8080 {
|
||||||
|
t.Error("Default Port not 8080")
|
||||||
|
}
|
||||||
|
if len(cfg.HoneyPots) != 1 || cfg.HoneyPots[0] != "_t_email" {
|
||||||
|
t.Error("Default HoneyPots are wrong")
|
||||||
|
}
|
||||||
|
if cfg.SmtpPort != 587 {
|
||||||
|
t.Error("SMTP Port not 587")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Correct config parsing", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
_ = os.Setenv("PORT", "1111")
|
||||||
|
_ = os.Setenv("HONEYPOTS", "pot,abc")
|
||||||
|
_ = os.Setenv("DEFAULT_TO", "mail@example.com")
|
||||||
|
_ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com")
|
||||||
|
_ = os.Setenv("EMAIL_FROM", "forms@example.com")
|
||||||
|
_ = os.Setenv("SMTP_USER", "test@example.com")
|
||||||
|
_ = os.Setenv("SMTP_PASS", "secret")
|
||||||
|
_ = os.Setenv("SMTP_HOST", "smtp.example.com")
|
||||||
|
_ = os.Setenv("SMTP_PORT", "100")
|
||||||
|
cfg, err := parseConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.Port, 1111) {
|
||||||
|
t.Error("Port is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.HoneyPots, []string{"pot", "abc"}) {
|
||||||
|
t.Error("HoneyPots are wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.DefaultRecipient, "mail@example.com") {
|
||||||
|
t.Error("DefaultRecipient is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.AllowedRecipients, []string{"mail@example.com", "test@example.com"}) {
|
||||||
|
t.Error("AllowedRecipients are wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.Sender, "forms@example.com") {
|
||||||
|
t.Error("Sender is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.SmtpUser, "test@example.com") {
|
||||||
|
t.Error("SMTP user is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.SmtpPassword, "secret") {
|
||||||
|
t.Error("SMTP password is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.SmtpHost, "smtp.example.com") {
|
||||||
|
t.Error("SMTP host is wrong")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.SmtpPort, 100) {
|
||||||
|
t.Error("SMTP port is wrong")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Error when wrong config", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
_ = os.Setenv("PORT", "ABC")
|
||||||
|
_, err := parseConfig()
|
||||||
|
if err == nil {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_checkRequiredConfig(t *testing.T) {
|
||||||
|
validConfig := config{
|
||||||
|
Port: 8080,
|
||||||
|
HoneyPots: []string{"_t_email"},
|
||||||
|
DefaultRecipient: "mail@example.com",
|
||||||
|
AllowedRecipients: []string{"mail@example.com"},
|
||||||
|
Sender: "forms@example.com",
|
||||||
|
SmtpUser: "test@example.com",
|
||||||
|
SmtpPassword: "secret",
|
||||||
|
SmtpHost: "smtp.example.com",
|
||||||
|
SmtpPort: 587,
|
||||||
|
}
|
||||||
|
t.Run("Valid config", func(t *testing.T) {
|
||||||
|
if true != checkRequiredConfig(validConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Default recipient missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.DefaultRecipient = ""
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Allowed recipients missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.AllowedRecipients = nil
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Sender missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.Sender = ""
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SMTP user missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.SmtpUser = ""
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SMTP password missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.SmtpPassword = ""
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SMTP host missing", func(t *testing.T) {
|
||||||
|
newConfig := validConfig
|
||||||
|
newConfig.SmtpHost = ""
|
||||||
|
if false != checkRequiredConfig(newConfig) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Test-Form</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action="//localhost:8080" method="post">
|
||||||
|
<input type="hidden" name="_to" value="test@example.com">
|
||||||
|
<input type="hidden" name="_formName" value="Test Form">
|
||||||
|
<input type="hidden" name="_redirectTo" value="https://example.com">
|
||||||
|
<label for="TEmail" style="display: none;">Test</label>
|
||||||
|
<input id="TEmail" type="email" name="_t_email" style="display: none;">
|
||||||
|
<label for="Email">Email</label>
|
||||||
|
<br />
|
||||||
|
<input id="Email" type="email" name="_replyTo" required>
|
||||||
|
<br />
|
||||||
|
<label for="Name">Name</label>
|
||||||
|
<br />
|
||||||
|
<input id="Name" type="text" name="Name" required>
|
||||||
|
<br />
|
||||||
|
<label for="Message">Message</label>
|
||||||
|
<br />
|
||||||
|
<textarea id="Message" name="Message"></textarea>
|
||||||
|
<br />
|
||||||
|
<input type="submit" value="Send">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormValues map[string][]string
|
||||||
|
|
||||||
|
func FormHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
_, _ = w.Write([]byte("MailyGo works!"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = r.ParseForm()
|
||||||
|
sanitizedForm := sanitizeForm(r.PostForm)
|
||||||
|
if !isBot(sanitizedForm) {
|
||||||
|
sendForm(sanitizedForm)
|
||||||
|
}
|
||||||
|
sendResponse(sanitizedForm, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeForm(values url.Values) FormValues {
|
||||||
|
p := bluemonday.StrictPolicy()
|
||||||
|
sanitizedForm := make(FormValues)
|
||||||
|
for key, values := range values {
|
||||||
|
var sanitizedValues []string
|
||||||
|
for _, value := range values {
|
||||||
|
sanitizedValues = append(sanitizedValues, p.Sanitize(value))
|
||||||
|
}
|
||||||
|
sanitizedForm[p.Sanitize(key)] = sanitizedValues
|
||||||
|
}
|
||||||
|
return sanitizedForm
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBot(values FormValues) bool {
|
||||||
|
for _, honeyPot := range appConfig.HoneyPots {
|
||||||
|
if len(values[honeyPot]) > 0 {
|
||||||
|
for _, value := range values[honeyPot] {
|
||||||
|
if value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendResponse(values FormValues, w http.ResponseWriter) {
|
||||||
|
if len(values["_redirectTo"]) == 1 && values["_redirectTo"][0] != "" {
|
||||||
|
w.Header().Add("Location", values["_redirectTo"][0])
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
_, _ = w.Write([]byte("Go to " + values["_redirectTo"][0]))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write([]byte("Submitted form"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_sanitizeForm(t *testing.T) {
|
||||||
|
t.Run("Sanitize form", func(t *testing.T) {
|
||||||
|
result := sanitizeForm(url.Values{"<b>Test</b>": {"<a href=\"https://example.com\">Test</a>"}})
|
||||||
|
want := FormValues{"Test": {"Test"}}
|
||||||
|
if !reflect.DeepEqual(result, want) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormHandler(t *testing.T) {
|
||||||
|
t.Run("GET request to FormHandler", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
FormHandler(w, req)
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("POST request to FormHandler", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("POST", "http://example.com/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
FormHandler(w, req)
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Wrong method request to FormHandler", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("DELETE", "http://example.com/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
FormHandler(w, req)
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isBot(t *testing.T) {
|
||||||
|
t.Run("No bot", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
result := isBot(FormValues{"_t_email": {""}})
|
||||||
|
if !reflect.DeepEqual(result, false) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("No honeypot", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
result := isBot(FormValues{})
|
||||||
|
if !reflect.DeepEqual(result, false) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Bot", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
result := isBot(FormValues{"_t_email": {"Test", ""}})
|
||||||
|
if !reflect.DeepEqual(result, true) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_sendResponse(t *testing.T) {
|
||||||
|
t.Run("No redirect", func(t *testing.T) {
|
||||||
|
values := FormValues{}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
sendResponse(values, w)
|
||||||
|
if w.Code != http.StatusCreated {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("No redirect 2", func(t *testing.T) {
|
||||||
|
values := FormValues{
|
||||||
|
"_redirectTo": {""},
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
sendResponse(values, w)
|
||||||
|
if w.Code != http.StatusCreated {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("No redirect 3", func(t *testing.T) {
|
||||||
|
values := FormValues{
|
||||||
|
"_redirectTo": {"abc", "def"},
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
sendResponse(values, w)
|
||||||
|
if w.Code != http.StatusCreated {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Redirect", func(t *testing.T) {
|
||||||
|
values := FormValues{
|
||||||
|
"_redirectTo": {"https://example.com"},
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
sendResponse(values, w)
|
||||||
|
if w.Code != http.StatusSeeOther {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if w.Header().Get("Location") != "https://example.com" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
module codeberg.org/jlelse/mailygo
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/caarlos0/env/v6 v6.2.1
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/caarlos0/env/v6 v6.2.1 h1:/bFpX1dg4TNioJjg7mrQaSrBoQvRfLUHNfXivdFbbEo=
|
||||||
|
github.com/caarlos0/env/v6 v6.2.1/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw=
|
||||||
|
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
|
||||||
|
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 h1:hJde9rA24hlTcAYSwJoXpDUyGtfKQ/jsofw+WaDqGrI=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendForm(values FormValues) {
|
||||||
|
recipient := findRecipient(values)
|
||||||
|
sendMail(recipient, buildMessage(recipient, time.Now(), values))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMessage(recipient string, date time.Time, values FormValues) string {
|
||||||
|
var msgBuffer bytes.Buffer
|
||||||
|
_, _ = fmt.Fprintf(&msgBuffer, "From: Forms <%s>", appConfig.Sender)
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
_, _ = fmt.Fprintf(&msgBuffer, "To: %s", recipient)
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
if replyTo := findReplyTo(values); replyTo != "" {
|
||||||
|
_, _ = fmt.Fprintf(&msgBuffer, "Reply-To: %s", replyTo)
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(&msgBuffer, "Date: %s", date.Format(time.RFC1123Z))
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
_, _ = fmt.Fprintf(&msgBuffer, "Subject: New submission on %s", findFormName(values))
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer)
|
||||||
|
for key, value := range removeMetaValues(values) {
|
||||||
|
_, _ = fmt.Fprint(&msgBuffer, key)
|
||||||
|
_, _ = fmt.Fprint(&msgBuffer, ": ")
|
||||||
|
_, _ = fmt.Fprintln(&msgBuffer, strings.Join(value, ", "))
|
||||||
|
}
|
||||||
|
return msgBuffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMail(to, message string) {
|
||||||
|
auth := smtp.PlainAuth("", appConfig.SmtpUser, appConfig.SmtpPassword, appConfig.SmtpHost)
|
||||||
|
err := smtp.SendMail(appConfig.SmtpHost+":"+strconv.Itoa(appConfig.SmtpPort), auth, appConfig.Sender, []string{to}, []byte(message))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to send mail:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRecipient(values FormValues) string {
|
||||||
|
if len(values["_to"]) == 1 && values["_to"][0] != "" {
|
||||||
|
formDefinedRecipient := values["_to"][0]
|
||||||
|
for _, allowed := range appConfig.AllowedRecipients {
|
||||||
|
if formDefinedRecipient == allowed {
|
||||||
|
return formDefinedRecipient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appConfig.DefaultRecipient
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFormName(values FormValues) string {
|
||||||
|
if len(values["_formName"]) == 1 && values["_formName"][0] != "" {
|
||||||
|
return values["_formName"][0]
|
||||||
|
}
|
||||||
|
return "a form"
|
||||||
|
}
|
||||||
|
|
||||||
|
func findReplyTo(values FormValues) string {
|
||||||
|
if len(values["_replyTo"]) == 1 && values["_replyTo"][0] != "" {
|
||||||
|
return values["_replyTo"][0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMetaValues(values FormValues) FormValues {
|
||||||
|
cleanedValues := FormValues{}
|
||||||
|
for key, value := range values {
|
||||||
|
if !strings.HasPrefix(key, "_") {
|
||||||
|
cleanedValues[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanedValues
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_findRecipient(t *testing.T) {
|
||||||
|
prepare := func() {
|
||||||
|
os.Clearenv()
|
||||||
|
_ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com")
|
||||||
|
_ = os.Setenv("DEFAULT_TO", "mail@example.com")
|
||||||
|
appConfig, _ = parseConfig()
|
||||||
|
}
|
||||||
|
t.Run("No recipient specified", func(t *testing.T) {
|
||||||
|
prepare()
|
||||||
|
values := FormValues{}
|
||||||
|
result := findRecipient(values)
|
||||||
|
if result != "mail@example.com" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Multiple recipients specified", func(t *testing.T) {
|
||||||
|
prepare()
|
||||||
|
values := FormValues{
|
||||||
|
"_to": {"abc@example.com", "def@example.com"},
|
||||||
|
}
|
||||||
|
result := findRecipient(values)
|
||||||
|
if result != "mail@example.com" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Allowed recipient specified", func(t *testing.T) {
|
||||||
|
prepare()
|
||||||
|
values := FormValues{
|
||||||
|
"_to": {"test@example.com"},
|
||||||
|
}
|
||||||
|
result := findRecipient(values)
|
||||||
|
if result != "test@example.com" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Forbidden recipient specified", func(t *testing.T) {
|
||||||
|
prepare()
|
||||||
|
values := FormValues{
|
||||||
|
"_to": {"forbidden@example.com"},
|
||||||
|
}
|
||||||
|
result := findRecipient(values)
|
||||||
|
if result != "mail@example.com" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findFormName(t *testing.T) {
|
||||||
|
t.Run("No form name", func(t *testing.T) {
|
||||||
|
if "a form" != findFormName(FormValues{}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Multiple form names", func(t *testing.T) {
|
||||||
|
if "a form" != findFormName(FormValues{"_formName": {"Test", "ABC"}}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Form name", func(t *testing.T) {
|
||||||
|
if "Test" != findFormName(FormValues{"_formName": {"Test"}}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findReplyTo(t *testing.T) {
|
||||||
|
t.Run("No replyTo", func(t *testing.T) {
|
||||||
|
if "" != findReplyTo(FormValues{}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Multiple replyTo", func(t *testing.T) {
|
||||||
|
if "" != findReplyTo(FormValues{"_replyTo": {"test@example.com", "test2@example.com"}}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("replyTo", func(t *testing.T) {
|
||||||
|
if "test@example.com" != findReplyTo(FormValues{"_replyTo": {"test@example.com"}}) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_removeMetaValues(t *testing.T) {
|
||||||
|
t.Run("Remove meta values", func(t *testing.T) {
|
||||||
|
result := removeMetaValues(FormValues{
|
||||||
|
"_test": {"abc"},
|
||||||
|
"test": {"def"},
|
||||||
|
})
|
||||||
|
want := FormValues{
|
||||||
|
"test": {"def"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result, want) {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_buildMessage(t *testing.T) {
|
||||||
|
t.Run("Test message", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
_ = os.Setenv("EMAIL_TO", "mail@example.com")
|
||||||
|
_ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com")
|
||||||
|
_ = os.Setenv("EMAIL_FROM", "forms@example.com")
|
||||||
|
appConfig, _ = parseConfig()
|
||||||
|
values := FormValues{
|
||||||
|
"_formName": {"Testform"},
|
||||||
|
"_replyTo": {"reply@example.com"},
|
||||||
|
"Testkey": {"Testvalue"},
|
||||||
|
"Another Key": {"Test", "ABC"},
|
||||||
|
}
|
||||||
|
date := time.Now()
|
||||||
|
result := buildMessage("test@example.com", date, values)
|
||||||
|
if !strings.Contains(result, "Reply-To: reply@example.com") {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "Subject: New submission on Testform") {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "Testkey: Testvalue") {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "Another Key: Test, ABC") {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appConfig config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, err := parseConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
appConfig = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if !checkRequiredConfig(appConfig) {
|
||||||
|
log.Fatal("Not all required configurations are set")
|
||||||
|
}
|
||||||
|
http.HandleFunc("/", FormHandler)
|
||||||
|
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(appConfig.Port), nil))
|
||||||
|
}
|
Reference in New Issue