mirror of https://github.com/jlelse/GoBlog
Improve the mock SMTP mechanism a lot
This commit is contained in:
parent
4857a82493
commit
42e218746b
|
@ -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 {
|
||||||
|
|
15
contact.go
15
contact.go
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
5
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/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=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue