diff --git a/config.go b/config.go index 65f8464..bd6d6ab 100644 --- a/config.go +++ b/config.go @@ -178,6 +178,7 @@ type configContact struct { SMTPPassword string `mapstructure:"smtpPassword"` EmailFrom string `mapstructure:"emailFrom"` EmailTo string `mapstructure:"emailTo"` + EmailSubject string `mapstructure:"emailSubject"` } type configAnnouncement struct { diff --git a/contact.go b/contact.go index 180361f..50d5e84 100644 --- a/contact.go +++ b/contact.go @@ -5,9 +5,11 @@ import ( "fmt" "log" "net/http" - "net/smtp" "strconv" "time" + + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" ) const defaultContactPath = "/contact" @@ -82,13 +84,18 @@ func (a *goBlog) sendContactEmail(cc *configContact, body, replyTo string) error _, _ = fmt.Fprintf(&email, "Reply-To: %s\n", replyTo) } _, _ = fmt.Fprintf(&email, "Date: %s\n", time.Now().UTC().Format(time.RFC1123Z)) - _, _ = fmt.Fprintf(&email, "Subject: New message\n\n") + _, _ = fmt.Fprintf(&email, "From: %s\n", cc.EmailFrom) + subject := cc.EmailSubject + if subject == "" { + subject = "New contact message" + } + _, _ = fmt.Fprintf(&email, "Subject: %s\n\n", subject) _, _ = fmt.Fprintf(&email, "%s\n", body) // Send email using SMTP - auth := smtp.PlainAuth("", cc.SMTPUser, cc.SMTPPassword, cc.SMTPHost) + auth := sasl.NewPlainClient("", cc.SMTPUser, cc.SMTPPassword) port := cc.SMTPPort if port == 0 { port = 587 } - return smtp.SendMail(cc.SMTPHost+":"+strconv.Itoa(port), auth, cc.EmailFrom, []string{cc.EmailTo}, email.Bytes()) + return smtp.SendMail(cc.SMTPHost+":"+strconv.Itoa(port), auth, cc.EmailFrom, []string{cc.EmailTo}, bytes.NewReader(email.Bytes())) } diff --git a/contact_test.go b/contact_test.go index 0b43195..7d2356e 100644 --- a/contact_test.go +++ b/contact_test.go @@ -18,8 +18,9 @@ import ( func Test_contact(t *testing.T) { // Start the SMTP server - port, rd, err := mocksmtp.StartMockSMTPServer() + port, rd, cancel, err := mocksmtp.StartMockSMTPServer() require.NoError(t, err) + defer cancel() // Init everything app := &goBlog{ @@ -42,6 +43,7 @@ func Test_contact(t *testing.T) { SMTPPassword: "pass", EmailTo: "to@example.org", EmailFrom: "from@example.org", + EmailSubject: "Neue Kontaktnachricht", }, }, }, @@ -65,11 +67,16 @@ func Test_contact(t *testing.T) { require.Equal(t, http.StatusOK, rec.Result().StatusCode) // Check sent mail - received, err := rd() - require.NoError(t, err) - assert.Contains(t, received, "This is a test contact message") - assert.Contains(t, received, "test@example.net") - assert.Contains(t, received, "https://test.example.com") - assert.Contains(t, received, "Test User") + assert.Contains(t, rd.Usernames, "user") + assert.Contains(t, rd.Passwords, "pass") + assert.Contains(t, rd.Froms, "from@example.org") + assert.Contains(t, rd.Rcpts, "to@example.org") + if assert.Len(t, rd.Datas, 1) { + assert.Contains(t, string(rd.Datas[0]), "This is a test contact message") + assert.Contains(t, string(rd.Datas[0]), "test@example.net") + assert.Contains(t, string(rd.Datas[0]), "https://test.example.com") + assert.Contains(t, string(rd.Datas[0]), "Test User") + assert.Contains(t, string(rd.Datas[0]), "Neue Kontaktnachricht") + } } diff --git a/example-config.yml b/example-config.yml index 6fdbba6..3043afa 100644 --- a/example-config.yml +++ b/example-config.yml @@ -242,6 +242,7 @@ blogs: smtpPassword: secret # SMTP password emailFrom: blog@example.com # Email sender emailTo: mail@example.com # Email recipient + emailSubject: "New contact message" # (Optional) Email subject # Announcement announcement: text: This is an **announcement**! # Can be markdown with links etc. \ No newline at end of file diff --git a/go.mod b/go.mod index a81c2c8..66892ca 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( github.com/dgraph-io/ristretto v0.1.0 github.com/dmulholl/mp3lib v1.0.0 github.com/elnormous/contenttype v1.0.0 + github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac + github.com/emersion/go-smtp v0.15.0 github.com/go-chi/chi/v5 v5.0.5 github.com/go-fed/httpsig v1.1.0 github.com/gorilla/handlers v1.5.1 diff --git a/go.sum b/go.sum index 2ed7a97..98ce002 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,11 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elnormous/contenttype v1.0.0 h1:cTLou7K7uQMsPEmRiTJosAznsPcYuoBmXMrFAf86t2A= github.com/elnormous/contenttype v1.0.0/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= +github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/pkgs/mocksmtp/backend.go b/pkgs/mocksmtp/backend.go new file mode 100644 index 0000000..6855958 --- /dev/null +++ b/pkgs/mocksmtp/backend.go @@ -0,0 +1,68 @@ +package mocksmtp + +import ( + "io" + "io/ioutil" + + "github.com/emersion/go-smtp" +) + +// ReceivedValues contains all the data received from the SMTP server +type ReceivedValues struct { + Usernames []string + Passwords []string + Froms []string + Rcpts []string + Datas [][]byte +} + +type backend struct { + values *ReceivedValues +} + +var _ smtp.Backend = &backend{} + +func (b *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) { + b.values.Usernames = append(b.values.Usernames, username) + b.values.Passwords = append(b.values.Passwords, password) + return &session{ + values: b.values, + }, nil +} + +func (b *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) { + return &session{ + values: b.values, + }, nil +} + +type session struct { + values *ReceivedValues +} + +var _ smtp.Session = &session{} + +func (s *session) Mail(from string, _ smtp.MailOptions) error { + s.values.Froms = append(s.values.Froms, from) + return nil +} + +func (s *session) Rcpt(to string) error { + s.values.Rcpts = append(s.values.Rcpts, to) + return nil +} + +func (s *session) Data(r io.Reader) error { + if b, err := ioutil.ReadAll(r); err != nil { + return err + } else { + s.values.Datas = append(s.values.Datas, b) + } + return nil +} + +func (s *session) Reset() {} + +func (s *session) Logout() error { + return nil +} diff --git a/pkgs/mocksmtp/mocksmtp.go b/pkgs/mocksmtp/mocksmtp.go index 91997c4..289db45 100644 --- a/pkgs/mocksmtp/mocksmtp.go +++ b/pkgs/mocksmtp/mocksmtp.go @@ -3,116 +3,62 @@ package mocksmtp import ( - "bufio" - "bytes" - "fmt" + "log" "net" - "net/textproto" "strconv" + + "github.com/emersion/go-smtp" ) -// Inspired by https://play.golang.org/p/8mfrqNVWTPK - -type ReceivedGetter func() (string, error) - -func StartMockSMTPServer() (port int, rg ReceivedGetter, err error) { - - // Default server responses - serverResponses := []string{ - "220 smtp.example.com Service ready", - "250-ELHO -> ok", - "250-Show Options for ESMTP", - "250-8BITMIME", - "250-SIZE", - "250-AUTH LOGIN PLAIN", - "250 HELP", - "235 AUTH -> ok", - "250 MAIL FROM -> ok", - "250 RCPT TO -> ok", - "354 DATA", - "250 ... -> ok", - "221 QUIT", - } - - // Channel to check if error occured or done - var errOrDone = make(chan error) - - // Received data buffer - var buffer bytes.Buffer - bufferWriter := bufio.NewWriter(&buffer) +// Start a mock SMTP server on a random port +// +// Returns: +// port: the port the server is listening on, +// receivedValues: struct to read the received values like username, password, data, +// cancelFunc: function to stop the server, +// err: something went wrong +func StartMockSMTPServer() (port int, receivedValues *ReceivedValues, cancelFunc func(), err error) { // Start server on random port mockSmtpServer, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - return 0, nil, err + return 0, nil, nil, err } // Get port from listener _, portStr, err := net.SplitHostPort(mockSmtpServer.Addr().String()) if err != nil { - return 0, nil, err + return 0, nil, nil, err } port, err = strconv.Atoi(portStr) if err != nil { - return 0, nil, err + return 0, nil, nil, err } - // Run mock SMTP server + // Define received values + receivedValues = &ReceivedValues{} + + // Init SMTP server + s := smtp.NewServer(&backend{ + values: receivedValues, + }) + s.Addr = mockSmtpServer.Addr().String() + s.Domain = "127.0.0.1" + s.AllowInsecureAuth = true + + // Start SMTP server go func() { - defer close(errOrDone) - defer bufferWriter.Flush() - defer mockSmtpServer.Close() - - conn, err := mockSmtpServer.Accept() - if err != nil { - errOrDone <- err - return - } - defer conn.Close() - - tc := textproto.NewConn(conn) - defer tc.Close() - - for _, res := range serverResponses { - if res == "" { - break - } - - _ = tc.PrintfLine("%s", res) - - if len(res) >= 4 && res[3] == '-' { - continue - } - - if res == "221 QUIT" { - return - } - - for { - msg, err := tc.ReadLine() - if err != nil { - errOrDone <- err - return - } - - _, _ = fmt.Fprintf(bufferWriter, "%s\n", msg) - - if res != "354 DATA" || msg == "." { - break - } - } + if err := s.Serve(mockSmtpServer); err != nil { + log.Fatal(err) } }() - // Define function to get received data - getReceivedData := func() (string, error) { - err, hasErr := <-errOrDone - if hasErr { - return "", err - } - return buffer.String(), nil + // Create cancel function + cancelFunc = func() { + _ = s.Close() } // Return port and function - return port, getReceivedData, nil + return port, receivedValues, cancelFunc, nil + } diff --git a/pkgs/mocksmtp/mocksmtp_test.go b/pkgs/mocksmtp/mocksmtp_test.go index 81cac79..c9df5f2 100644 --- a/pkgs/mocksmtp/mocksmtp_test.go +++ b/pkgs/mocksmtp/mocksmtp_test.go @@ -11,22 +11,26 @@ import ( func Test_mocksmtp(t *testing.T) { // Start mock SMTP server - port, getRecievedData, err := StartMockSMTPServer() + port, rd, cancel, err := StartMockSMTPServer() require.NoError(t, err) + defer cancel() // Send mail err = smtp.SendMail( fmt.Sprintf("127.0.0.1:%d", port), smtp.PlainAuth("", "user", "pass", "127.0.0.1"), - "admin@smtp.example.com", - []string{"user@smtp.example.com"}, - []byte("From: admin@smtp.example.com\nTo: user@smtp.example.com\nSubject: Test\nMIME-version: 1.0\nContent-Type: text/html; charset=\"UTF-8\"\n\nThis is a test mail."), + "admin@example.com", + []string{"user@example.com"}, + []byte("From: admin@example.com\nTo: user@example.com\nSubject: Test\nMIME-version: 1.0\nContent-Type: text/html; charset=\"UTF-8\"\n\nThis is a test mail."), ) require.NoError(t, err) // Get received data - recievedData, err := getRecievedData() - require.NoError(t, err) - assert.Contains(t, recievedData, "From:") - assert.Contains(t, recievedData, "This is a test mail") + assert.Contains(t, rd.Froms, "admin@example.com") + assert.Contains(t, rd.Rcpts, "user@example.com") + assert.Contains(t, rd.Usernames, "user") + assert.Contains(t, rd.Passwords, "pass") + if assert.Len(t, rd.Froms, 1) { + assert.Contains(t, string(rd.Datas[0]), "This is a test mail") + } }