Improve the mock SMTP mechanism a lot

This commit is contained in:
Jan-Lukas Else 2021-11-08 16:27:33 +01:00
parent 4857a82493
commit 42e218746b
9 changed files with 147 additions and 106 deletions

View File

@ -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 {

View File

@ -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()))
}

View File

@ -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")
}
}

View File

@ -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.

2
go.mod
View File

@ -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

5
go.sum
View File

@ -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=

68
pkgs/mocksmtp/backend.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}