Adding upstream version 3.6.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
1347af6294
commit
cb9cbb7a25
32 changed files with 4944 additions and 0 deletions
453
sender_test.go
Normal file
453
sender_test.go
Normal file
|
@ -0,0 +1,453 @@
|
|||
package mailyak
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Test RSA key & self-signed certificate populated by init()
|
||||
testRSAKey *rsa.PrivateKey
|
||||
testCertBytes []byte
|
||||
testCert *x509.Certificate
|
||||
)
|
||||
|
||||
// Initialise the TLS certificate and key material for TLS tests.
|
||||
func init() {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to generate RSA test key: %v", err))
|
||||
}
|
||||
testRSAKey = key
|
||||
|
||||
// Define the certificate template
|
||||
self := &x509.Certificate{
|
||||
Version: 3,
|
||||
SerialNumber: big.NewInt(42),
|
||||
Issuer: pkix.Name{
|
||||
CommonName: "CA Bananas Inc",
|
||||
},
|
||||
Subject: pkix.Name{
|
||||
CommonName: "The Banana Factory",
|
||||
},
|
||||
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
IPAddresses: []net.IP{
|
||||
net.IPv4(127, 0, 0, 1),
|
||||
},
|
||||
}
|
||||
|
||||
// Sign the template certificate
|
||||
cert, err := x509.CreateCertificate(rand.Reader, self, self, &testRSAKey.PublicKey, testRSAKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to generate self-signed test cert: %v", err))
|
||||
}
|
||||
testCertBytes = cert
|
||||
|
||||
// Parse the signed certificate
|
||||
serverCert, err := x509.ParseCertificate(testCertBytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to bind to localhost: %v", err))
|
||||
}
|
||||
testCert = serverCert
|
||||
}
|
||||
|
||||
// connAsserts wraps a net.Conn, performing writes and asserts responses within
|
||||
// tests.
|
||||
//
|
||||
// Any errors cause the method to panic.
|
||||
type connAsserts struct {
|
||||
net.Conn
|
||||
|
||||
t *testing.T
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *connAsserts) Read(b []byte) (n int, err error) {
|
||||
n, err = c.Conn.Read(b)
|
||||
c.t.Logf("MailYak -> Server:\n%s\n", hex.Dump(b))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *connAsserts) Write(b []byte) (n int, err error) {
|
||||
n, err = c.Conn.Write(b)
|
||||
c.t.Logf("Server -> MailYak:\n%s\n", hex.Dump(b))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *connAsserts) Expect(want string) {
|
||||
c.buf.Reset()
|
||||
|
||||
n, err := io.CopyN(c.buf, c, int64(len(want)))
|
||||
if err != nil {
|
||||
c.t.Fatalf("got error %v after reading %d bytes (got %q, want %q)", err, n, c.buf.String(), want)
|
||||
}
|
||||
if c.buf.String() != want {
|
||||
c.t.Fatalf("read %q, want %q", c.buf.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connAsserts) Respond(put string) {
|
||||
n, err := c.Write([]byte(put))
|
||||
if err != nil {
|
||||
c.t.Fatalf("got error %v writing %q (wrote %d bytes)", err, put, n)
|
||||
}
|
||||
}
|
||||
|
||||
func newConnAsserts(c net.Conn, t *testing.T) *connAsserts {
|
||||
return &connAsserts{
|
||||
Conn: c,
|
||||
t: t,
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
}
|
||||
|
||||
// mockMail provides the methods for a sendableMail, allowing for deterministic
|
||||
// MIME content in tests.
|
||||
type mockMail struct {
|
||||
localName string
|
||||
toAddrs []string
|
||||
fromAddr string
|
||||
auth smtp.Auth
|
||||
mime string
|
||||
}
|
||||
|
||||
// getLocalName should return the sender domain to be used in the EHLO/HELO
|
||||
// command.
|
||||
func (m *mockMail) getLocalName() string {
|
||||
return m.localName
|
||||
}
|
||||
|
||||
// toAddrs should return a slice of email addresses to be added to the RCPT
|
||||
// TO command.
|
||||
func (m *mockMail) getToAddrs() []string {
|
||||
return stripNames(m.toAddrs)
|
||||
}
|
||||
|
||||
// fromAddr should return the address to be used in the MAIL FROM command.
|
||||
func (m *mockMail) getFromAddr() string {
|
||||
return m.fromAddr
|
||||
}
|
||||
|
||||
// auth should return the smtp.Auth if configured, nil if not.
|
||||
func (m *mockMail) getAuth() smtp.Auth {
|
||||
return m.auth
|
||||
}
|
||||
|
||||
// buildMime should write the generated MIME to w.
|
||||
//
|
||||
// The emailSender implementation is responsible for providing appropriate
|
||||
// buffering of writes.
|
||||
func (m *mockMail) buildMime(w io.Writer) error {
|
||||
_, err := w.Write([]byte(m.mime))
|
||||
return err
|
||||
}
|
||||
|
||||
// TestSMTPProtocolExchange sends the same mock email over two different
|
||||
// transports using two different sender implementations, ensuring parity
|
||||
// between the two (specifically that both impleementations result in the same
|
||||
// SMTP conversation).
|
||||
//
|
||||
// Because the mock server in the tests does not advertise STARTTLS support in,
|
||||
// there is no upgrade.
|
||||
func TestSMTPProtocolExchange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testTimeout = 15 * time.Second
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mail *mockMail
|
||||
|
||||
// Called once the Send() method is invoked, impersonating and asserting
|
||||
// the client/server conversation.
|
||||
connFn func(c *connAsserts)
|
||||
|
||||
// Error returned when sending over TLS
|
||||
wantTLSErr error
|
||||
|
||||
// Error returned when sending over plaintext
|
||||
wantPlaintextErr error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
mail: &mockMail{
|
||||
toAddrs: []string{
|
||||
"to@example.org",
|
||||
"another@example.com",
|
||||
"Dom <dom@itsallbroken.com>",
|
||||
},
|
||||
fromAddr: "from@example.org",
|
||||
mime: "bananas",
|
||||
},
|
||||
connFn: func(c *connAsserts) {
|
||||
c.Respond("220 localhost ESMTP bananas\r\n")
|
||||
|
||||
c.Expect("EHLO localhost\r\n")
|
||||
c.Respond("250-localhost Hola\r\n")
|
||||
c.Respond("250 AUTH LOGIN PLAIN\r\n")
|
||||
|
||||
c.Expect("MAIL FROM:<from@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<to@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<another@example.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<dom@itsallbroken.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("DATA\r\n")
|
||||
c.Respond("354 OK\r\n")
|
||||
c.Expect("bananas\r\n.\r\n")
|
||||
c.Respond("250 Will do friend\r\n")
|
||||
|
||||
c.Expect("QUIT\r\n")
|
||||
c.Respond("221 Adios\r\n")
|
||||
},
|
||||
wantTLSErr: nil,
|
||||
wantPlaintextErr: nil,
|
||||
},
|
||||
{
|
||||
name: "with auth",
|
||||
mail: &mockMail{
|
||||
toAddrs: []string{
|
||||
"to@example.org",
|
||||
"another@example.com",
|
||||
"dom@itsallbroken.com",
|
||||
},
|
||||
fromAddr: "from@example.org",
|
||||
mime: "bananas",
|
||||
auth: smtp.PlainAuth("ident", "user", "pass", "127.0.0.1"),
|
||||
},
|
||||
connFn: func(c *connAsserts) {
|
||||
c.Respond("220 localhost ESMTP bananas\r\n")
|
||||
|
||||
c.Expect("EHLO localhost\r\n")
|
||||
c.Respond("250-localhost Hola\r\n")
|
||||
c.Respond("250 AUTH LOGIN PLAIN\r\n")
|
||||
|
||||
c.Expect("AUTH PLAIN aWRlbnQAdXNlcgBwYXNz\r\n")
|
||||
c.Respond("235 Looks good\r\n")
|
||||
|
||||
c.Expect("MAIL FROM:<from@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<to@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<another@example.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<dom@itsallbroken.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("DATA\r\n")
|
||||
c.Respond("354 OK\r\n")
|
||||
c.Expect("bananas\r\n.\r\n")
|
||||
c.Respond("250 Will do friend\r\n")
|
||||
|
||||
c.Expect("QUIT\r\n")
|
||||
c.Respond("221 Adios\r\n")
|
||||
},
|
||||
wantTLSErr: nil,
|
||||
wantPlaintextErr: nil,
|
||||
},
|
||||
{
|
||||
name: "with localname",
|
||||
mail: &mockMail{
|
||||
toAddrs: []string{
|
||||
"to@example.org",
|
||||
"another@example.com",
|
||||
"Dom <dom@itsallbroken.com>",
|
||||
},
|
||||
fromAddr: "from@example.org",
|
||||
mime: "bananas",
|
||||
localName: "example.com",
|
||||
},
|
||||
connFn: func(c *connAsserts) {
|
||||
c.Respond("220 localhost ESMTP bananas\r\n")
|
||||
|
||||
c.Expect("EHLO example.com\r\n")
|
||||
c.Respond("250-example.com Hola\r\n")
|
||||
c.Respond("250 AUTH LOGIN PLAIN\r\n")
|
||||
|
||||
c.Expect("MAIL FROM:<from@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<to@example.org>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<another@example.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("RCPT TO:<dom@itsallbroken.com>\r\n")
|
||||
c.Respond("250 OK\r\n")
|
||||
|
||||
c.Expect("DATA\r\n")
|
||||
c.Respond("354 OK\r\n")
|
||||
c.Expect("bananas\r\n.\r\n")
|
||||
c.Respond("250 Will do friend\r\n")
|
||||
|
||||
c.Expect("QUIT\r\n")
|
||||
c.Respond("221 Adios\r\n")
|
||||
},
|
||||
wantTLSErr: nil,
|
||||
wantPlaintextErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// handleConn provides the accept loop for both the TLS server, and the
|
||||
// plain-text server, passing the accepted connection to the test actor
|
||||
// func.
|
||||
//
|
||||
// Once the actor func has finished, done is closed.
|
||||
handleConn := func(t *testing.T, l net.Listener, done chan<- struct{}, actor func(c *connAsserts)) {
|
||||
defer close(done)
|
||||
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
actor(newConnAsserts(conn, t))
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Send the mock email over each implementation of the sender
|
||||
// interface, including initialisation with the respective MailYak
|
||||
// constructor.
|
||||
|
||||
t.Run("Explicit_TLS", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Initialise a server TLS config using the self-signed test
|
||||
// certificate and key material.
|
||||
serverConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{testCertBytes},
|
||||
PrivateKey: testRSAKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Bind a TLS-enabled TCP socket to some random port
|
||||
socket, err := tls.Listen("tcp", "127.0.0.1:0", serverConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to bind to localhost: %v", err)
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
handlerDone := make(chan struct{})
|
||||
go handleConn(t, socket, handlerDone, tt.connFn)
|
||||
|
||||
// Build a root store for the self-signed certificate.
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(testCert)
|
||||
|
||||
// Initialise a TLS mailyak using the root store.
|
||||
m, err := NewWithTLS(socket.Addr().String(), nil, &tls.Config{
|
||||
RootCAs: roots,
|
||||
ServerName: "127.0.0.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Call into the sender directly, giving it the mock
|
||||
// sendableEmail
|
||||
sendErr := make(chan error)
|
||||
go func() {
|
||||
sendErr <- m.sender.Send(tt.mail)
|
||||
}()
|
||||
|
||||
// Wait for the SMTP conversation to complete
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for SMTP conversation to complete")
|
||||
case <-handlerDone:
|
||||
// The handler is complete, wait for the send error and
|
||||
// check it matches the expected value.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for Send() to return")
|
||||
|
||||
case err := <-sendErr:
|
||||
if !reflect.DeepEqual(err, tt.wantTLSErr) {
|
||||
t.Errorf("got %v, want %v", err, tt.wantTLSErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Plaintext", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start listening to a local plain-text socket
|
||||
socket, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to bind to localhost: %v", err)
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
handlerDone := make(chan struct{})
|
||||
go handleConn(t, socket, handlerDone, tt.connFn)
|
||||
|
||||
m := New(socket.Addr().String(), nil)
|
||||
|
||||
// Call into the sender directly, giving it the mock
|
||||
// sendableEmail
|
||||
sendErr := make(chan error)
|
||||
go func() {
|
||||
sendErr <- m.sender.Send(tt.mail)
|
||||
}()
|
||||
|
||||
// Wait for the SMTP conversation to complete
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for SMTP conversation to complete")
|
||||
case <-handlerDone:
|
||||
// The handler is complete, wait for the send error and
|
||||
// check it matches the expected value.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for Send() to return")
|
||||
|
||||
case err := <-sendErr:
|
||||
if !reflect.DeepEqual(err, tt.wantPlaintextErr) {
|
||||
t.Errorf("got %v, want %v", err, tt.wantPlaintextErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue