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"` SMTPPassword string `mapstructure:"smtpPassword"`
EmailFrom string `mapstructure:"emailFrom"` EmailFrom string `mapstructure:"emailFrom"`
EmailTo string `mapstructure:"emailTo"` EmailTo string `mapstructure:"emailTo"`
EmailSubject string `mapstructure:"emailSubject"`
} }
type configAnnouncement struct { type configAnnouncement struct {

View File

@ -5,9 +5,11 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/smtp"
"strconv" "strconv"
"time" "time"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
) )
const defaultContactPath = "/contact" 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, "Reply-To: %s\n", replyTo)
} }
_, _ = fmt.Fprintf(&email, "Date: %s\n", time.Now().UTC().Format(time.RFC1123Z)) _, _ = 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) _, _ = fmt.Fprintf(&email, "%s\n", body)
// Send email using SMTP // Send email using SMTP
auth := smtp.PlainAuth("", cc.SMTPUser, cc.SMTPPassword, cc.SMTPHost) auth := sasl.NewPlainClient("", cc.SMTPUser, cc.SMTPPassword)
port := cc.SMTPPort port := cc.SMTPPort
if port == 0 { if port == 0 {
port = 587 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) { func Test_contact(t *testing.T) {
// Start the SMTP server // Start the SMTP server
port, rd, err := mocksmtp.StartMockSMTPServer() port, rd, cancel, err := mocksmtp.StartMockSMTPServer()
require.NoError(t, err) require.NoError(t, err)
defer cancel()
// Init everything // Init everything
app := &goBlog{ app := &goBlog{
@ -42,6 +43,7 @@ func Test_contact(t *testing.T) {
SMTPPassword: "pass", SMTPPassword: "pass",
EmailTo: "to@example.org", EmailTo: "to@example.org",
EmailFrom: "from@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) require.Equal(t, http.StatusOK, rec.Result().StatusCode)
// Check sent mail // Check sent mail
received, err := rd() assert.Contains(t, rd.Usernames, "user")
require.NoError(t, err) assert.Contains(t, rd.Passwords, "pass")
assert.Contains(t, received, "This is a test contact message") assert.Contains(t, rd.Froms, "from@example.org")
assert.Contains(t, received, "test@example.net") assert.Contains(t, rd.Rcpts, "to@example.org")
assert.Contains(t, received, "https://test.example.com") if assert.Len(t, rd.Datas, 1) {
assert.Contains(t, received, "Test User") 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 smtpPassword: secret # SMTP password
emailFrom: blog@example.com # Email sender emailFrom: blog@example.com # Email sender
emailTo: mail@example.com # Email recipient emailTo: mail@example.com # Email recipient
emailSubject: "New contact message" # (Optional) Email subject
# Announcement # Announcement
announcement: announcement:
text: This is an **announcement**! # Can be markdown with links etc. 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/dgraph-io/ristretto v0.1.0
github.com/dmulholl/mp3lib v1.0.0 github.com/dmulholl/mp3lib v1.0.0
github.com/elnormous/contenttype 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-chi/chi/v5 v5.0.5
github.com/go-fed/httpsig v1.1.0 github.com/go-fed/httpsig v1.1.0
github.com/gorilla/handlers v1.5.1 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/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 h1:cTLou7K7uQMsPEmRiTJosAznsPcYuoBmXMrFAf86t2A=
github.com/elnormous/contenttype v1.0.0/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg= 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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 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 package mocksmtp
import ( import (
"bufio" "log"
"bytes"
"fmt"
"net" "net"
"net/textproto"
"strconv" "strconv"
"github.com/emersion/go-smtp"
) )
// Inspired by https://play.golang.org/p/8mfrqNVWTPK // Start a mock SMTP server on a random port
//
type ReceivedGetter func() (string, error) // Returns:
// port: the port the server is listening on,
func StartMockSMTPServer() (port int, rg ReceivedGetter, err error) { // receivedValues: struct to read the received values like username, password, data,
// cancelFunc: function to stop the server,
// Default server responses // err: something went wrong
serverResponses := []string{ func StartMockSMTPServer() (port int, receivedValues *ReceivedValues, cancelFunc func(), err error) {
"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 server on random port // Start server on random port
mockSmtpServer, err := net.Listen("tcp", "127.0.0.1:0") mockSmtpServer, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
return 0, nil, err return 0, nil, nil, err
} }
// Get port from listener // Get port from listener
_, portStr, err := net.SplitHostPort(mockSmtpServer.Addr().String()) _, portStr, err := net.SplitHostPort(mockSmtpServer.Addr().String())
if err != nil { if err != nil {
return 0, nil, err return 0, nil, nil, err
} }
port, err = strconv.Atoi(portStr) port, err = strconv.Atoi(portStr)
if err != nil { 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() { go func() {
defer close(errOrDone) if err := s.Serve(mockSmtpServer); err != nil {
defer bufferWriter.Flush() log.Fatal(err)
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
}
}
} }
}() }()
// Define function to get received data // Create cancel function
getReceivedData := func() (string, error) { cancelFunc = func() {
err, hasErr := <-errOrDone _ = s.Close()
if hasErr {
return "", err
}
return buffer.String(), nil
} }
// Return port and function // 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) { func Test_mocksmtp(t *testing.T) {
// Start mock SMTP server // Start mock SMTP server
port, getRecievedData, err := StartMockSMTPServer() port, rd, cancel, err := StartMockSMTPServer()
require.NoError(t, err) require.NoError(t, err)
defer cancel()
// Send mail // Send mail
err = smtp.SendMail( err = smtp.SendMail(
fmt.Sprintf("127.0.0.1:%d", port), fmt.Sprintf("127.0.0.1:%d", port),
smtp.PlainAuth("", "user", "pass", "127.0.0.1"), smtp.PlainAuth("", "user", "pass", "127.0.0.1"),
"admin@smtp.example.com", "admin@example.com",
[]string{"user@smtp.example.com"}, []string{"user@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."), []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) require.NoError(t, err)
// Get received data // Get received data
recievedData, err := getRecievedData() assert.Contains(t, rd.Froms, "admin@example.com")
require.NoError(t, err) assert.Contains(t, rd.Rcpts, "user@example.com")
assert.Contains(t, recievedData, "From:") assert.Contains(t, rd.Usernames, "user")
assert.Contains(t, recievedData, "This is a test mail") 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")
}
} }