diff --git a/app/config/config_test.go b/app/config/config_test.go index a52bd2c..4644b0a 100644 --- a/app/config/config_test.go +++ b/app/config/config_test.go @@ -13,7 +13,7 @@ func TestSmokeLoadConfigShouldBeOk(t *testing.T) { data := ` smtpd-proxy: listen: 127.0.0.1:1025 - ehlo: localhost + ehlo: 127.0.0.1 username: user password: secret server-cert: server.crt @@ -41,7 +41,7 @@ smtpd-proxy: srv := c.ServerConfig assert.Equal(t, "127.0.0.1:1025", srv.Listen) - assert.Equal(t, "localhost", srv.Ehlo) + assert.Equal(t, "127.0.0.1", srv.Ehlo) assert.Equal(t, "user", srv.Username) assert.Equal(t, "secret", srv.Password) assert.Equal(t, "server.crt", srv.ServerCertificatePath) diff --git a/app/main_test.go b/app/main_test.go index 4be362c..7914fed 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -25,7 +25,7 @@ func Test_Main(t *testing.T) { yamlConfig := fmt.Sprintf(` smtpd-proxy: listen: %s:%d - ehlo: localhost + ehlo: 127.0.0.1 username: user password: secret is_anon_auth_allowed: true @@ -73,7 +73,7 @@ smtpd-proxy: response := readStrings(bufReader) for _, s := range [...]string{ - "220 localhost ESMTP Service Ready", + "220 127.0.0.1 ESMTP Service Ready", "250-Hello test", "250-PIPELINING", "250-8BITMIME", @@ -100,7 +100,7 @@ func waitForPortListenStart(ctx context.Context, t *testing.T, port int) (conn n var d net.Dialer var err error addr := fmt.Sprintf("%s:%d", bindHost, port) - poll := time.NewTicker(20 * time.Millisecond) + poll := time.NewTicker(50 * time.Millisecond) defer poll.Stop() timeout := time.NewTimer(5 * time.Second) defer timeout.Stop() diff --git a/go.mod b/go.mod index be5575d..202cf16 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ses v1.18.1 github.com/creasty/defaults v1.7.0 github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead - github.com/emersion/go-smtp v0.18.1 + github.com/emersion/go-smtp v0.19.0 github.com/hashicorp/go-multierror v1.1.1 github.com/jessevdk/go-flags v1.5.0 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible diff --git a/go.sum b/go.sum index bcddf8c..e22f7dc 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.18.1 h1:4DFV0jxKhq0Gqt/Br3BRHyKZy5TStk6NIMHAx6GE/LA= -github.com/emersion/go-smtp v0.18.1/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-smtp v0.19.0 h1:iVCDtR2/JY3RpKoaZ7u6I/sb52S3EzfNHO1fAWVHgng= +github.com/emersion/go-smtp v0.19.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= diff --git a/tests/infra.go b/tests/infra.go index 94caea5..f21ac04 100644 --- a/tests/infra.go +++ b/tests/infra.go @@ -12,7 +12,6 @@ import ( "time" "github.com/leonardinius/smtpd-proxy/app/cmd" - "github.com/leonardinius/smtpd-proxy/app/zlog" "github.com/stretchr/testify/require" tc "github.com/testcontainers/testcontainers-go" ) @@ -34,6 +33,7 @@ func RunMainWithConfig(ctx context.Context, t *testing.T, yamlConfig string, por serverCh := make(chan cmd.ServerSignal) done := make(chan struct{}) + go func() { <-done serverCh <- cmd.ServerStopSignal @@ -52,11 +52,6 @@ func RunMainWithConfig(ctx context.Context, t *testing.T, yamlConfig string, por }() conn := waitForPortListenStart(ctx, t, port) - defer func() { - err = conn.Close() - zlog.Debugf("conn.Close() error: %v", err) - }() - test(t, conn) } @@ -81,7 +76,7 @@ func waitForPortListenStart(ctx context.Context, t *testing.T, port int) (conn n } require.NotNil(t, conn) - err = conn.SetDeadline(time.Now().Add(100 * time.Millisecond)) + err = conn.SetDeadline(time.Now().Add(500 * time.Millisecond)) if err != nil { t.Fatal("SMTP set connection deadline error", err) } diff --git a/tests/ses_forwarder_test.go b/tests/ses_forwarder_test.go index 8bd20e5..da3fd4a 100644 --- a/tests/ses_forwarder_test.go +++ b/tests/ses_forwarder_test.go @@ -62,13 +62,14 @@ func (su *SESSystemTestSuite) /* */ TearDownSuite() { func (su *SESSystemTestSuite) TestSmokeSESForwardAcceptsSimpleEMail() { port := DynamicPort() proxyEndpoint := fmt.Sprintf("%s:%d", BindHost, port) - sesEndpoint, err := su.localstack.PortEndpoint(su.ctx, "4566/tcp", "http") + sesPort, err := su.localstack.MappedPort(su.ctx, "4566/tcp") require.NoError(su.T(), err) + sesEndpoint := fmt.Sprintf("http://%s:%s", BindHost, sesPort.Port()) config := fmt.Sprintf(` smtpd-proxy: listen: %s:%d - ehlo: localhost - username: user@example.com + ehlo: 127.0.0.1 + username: user-ses@example.com password: password is_anon_auth_allowed: false upstream-servers: @@ -80,25 +81,25 @@ smtpd-proxy: region: us-east-1 `, BindHost, port, sesEndpoint) RunMainWithConfig(su.ctx, su.T(), config, port, func(t *testing.T, conn net.Conn) { - fromEmail := "" + fromEmail := fmt.Sprintf("", time.Now().UnixMilli()) // Setup authentication information. - auth := smtp.PlainAuth("", "user@example.com", "password", BindHost) - to := []string{"recipient@example.net"} + auth := smtp.PlainAuth("", "user-ses@example.com", "password", BindHost) + to := []string{"recipient-ses@example.net"} msg := strings.Join([]string{ - "To: ", + "To: ", "From: " + fromEmail, - "Subject: Test E-mail!", + "Subject: Test E-mail! (SES)", "", - "This is the email body.", + "This is the email body (SES).", "", }, "\r\n") - err := smtp.SendMail(proxyEndpoint, auth, "sender@example.org", to, []byte(msg)) - require.ErrorContains(t, err, "Email address not verified ") + err := smtp.SendMail(proxyEndpoint, auth, "sender-ses@example.org", to, []byte(msg)) + require.ErrorContains(t, err, "Email address not verified") ses := newSesClient(su.ctx, t, sesEndpoint) _, err = ses.VerifyEmailIdentity(su.ctx, &awsses.VerifyEmailIdentityInput{EmailAddress: aws.String(fromEmail)}) require.NoError(t, err) - err = smtp.SendMail(proxyEndpoint, auth, "sender@example.org", to, []byte(msg)) + err = smtp.SendMail(proxyEndpoint, auth, "sender-ses@example.org", to, []byte(msg)) assert.NoError(t, err) sesFile := requireSesFileWithContains(t, fromEmail) @@ -106,20 +107,21 @@ smtpd-proxy: require.NoError(t, err) jsonMessage := string(bytes) assert.Contains(t, jsonMessage, - "\"This is the email body.\\r\\n\"") + "\"This is the email body (SES).\\r\\n\"") }) } func (su *SESSystemTestSuite) TestSmokeSESForwardAcceptsEMailWithAttachments() { port := DynamicPort() proxyEndpoint := fmt.Sprintf("%s:%d", BindHost, port) - sesEndpoint, err := su.localstack.PortEndpoint(su.ctx, "4566/tcp", "http") + sesPort, err := su.localstack.MappedPort(su.ctx, "4566/tcp") require.NoError(su.T(), err) + sesEndpoint := fmt.Sprintf("http://%s:%s", BindHost, sesPort.Port()) config := fmt.Sprintf(` smtpd-proxy: listen: %s:%d - ehlo: localhost - username: user@example.com + ehlo: 127.0.0.1 + username: user-ses@example.com password: password is_anon_auth_allowed: false upstream-servers: @@ -131,18 +133,19 @@ smtpd-proxy: region: us-east-1 `, BindHost, port, sesEndpoint) RunMainWithConfig(su.ctx, su.T(), config, port, func(t *testing.T, conn net.Conn) { + fromEmail := fmt.Sprintf("", time.Now().UnixMilli()) ses := newSesClient(su.ctx, t, sesEndpoint) - _, err = ses.VerifyEmailIdentity(su.ctx, &awsses.VerifyEmailIdentityInput{EmailAddress: aws.String("")}) - require.NoError(t, err, "failed to verify gotest-attachment@esmtp.email") + _, err = ses.VerifyEmailIdentity(su.ctx, &awsses.VerifyEmailIdentityInput{EmailAddress: aws.String(fromEmail)}) + require.NoError(t, err, "failed to verify attachment email") // Setup authentication information. - auth := smtp.PlainAuth("", "user@example.com", "password", BindHost) + auth := smtp.PlainAuth("", "user-ses@example.com", "password", BindHost) envelope := email.NewEmail() - envelope.To = []string{""} - envelope.From = "" - envelope.Subject = "Subject: Test E-mail!" - envelope.Text = []byte("This is the email body.") - envelope.Sender = "recipient@example.net" + envelope.To = []string{""} + envelope.From = fromEmail + envelope.Subject = "Subject: Test Ses E-mail!" + envelope.Text = []byte("This is the email body (SES).") + envelope.Sender = "recipient-ses@example.net" _, err := envelope.AttachFile("_testData/text-attachment.txt") require.NoError(t, err, "failed to attach file") err = envelope.Send(proxyEndpoint, auth) @@ -157,7 +160,7 @@ smtpd-proxy: for strings.HasSuffix(loremIpsumBase64, "=") { loremIpsumBase64 = strings.TrimSuffix(loremIpsumBase64, "=") } - assert.Contains(t, jsonMessage, "\\r\\nThis is the email body.\\r\\n") + assert.Contains(t, jsonMessage, "\\r\\nThis is the email body (SES).\\r\\n") assert.Contains(t, jsonMessage, loremIpsumBase64) }) } @@ -165,23 +168,24 @@ smtpd-proxy: func iniFakeSMTPContainer(ctx context.Context) (container tc.Container, err error) { vol, _ := filepath.Abs(".volume") _ = os.Mkdir(vol, 0o755) - _ = os.RemoveAll(filepath.Join(vol, "tmp", "state", "ses")) + _ = os.RemoveAll(vol + "/state/ses") localstackReq := tc.ContainerRequest{ - Image: "localstack/localstack", + Image: "localstack/localstack:2.3.2", ExposedPorts: []string{"4566/tcp"}, Env: map[string]string{ "EAGER_SERVICE_LOADING": "1", "SERVICES": "ses", + "DEBUG": "1", + "PERSISTENCE": "1", }, Mounts: tc.Mounts(tc.BindMount(vol, "/var/lib/localstack")), WaitingFor: wait.ForListeningPort("4566/tcp"), } - container, err = tc.GenericContainer(ctx, tc.GenericContainerRequest{ ContainerRequest: localstackReq, Started: true, }) - return + return container, err } func newSesClient(ctx context.Context, t *testing.T, endpoint string) *awsses.Client { @@ -205,7 +209,7 @@ func newSesClient(ctx context.Context, t *testing.T, endpoint string) *awsses.Cl func requireSesFileWithContains(t *testing.T, needle string) *os.File { var sesFile *os.File - const mailSesJSONDir = ".volume/tmp/state/ses/" + const mailSesJSONDir = ".volume/state/ses/" assert.Eventuallyf(t, func() bool { dir, e := filepath.Abs(mailSesJSONDir) @@ -233,8 +237,8 @@ func requireSesFileWithContains(t *testing.T, needle string) *os.File { } return sesFile != nil }, - 5*time.Second, - 50*time.Millisecond, + 10*time.Second, + 300*time.Millisecond, "Failed to obtain ses payloads for %s", needle, ) require.NotNil(t, sesFile) diff --git a/tests/smtp_forwarder_test.go b/tests/smtp_forwarder_test.go index 77845b1..0a4969c 100644 --- a/tests/smtp_forwarder_test.go +++ b/tests/smtp_forwarder_test.go @@ -58,18 +58,18 @@ func (su *SMTPSystemTestSuite) TearDownSuite() { func (su *SMTPSystemTestSuite) TestSmokeSMTPForwardSimpleEmail() { port := DynamicPort() proxyEndpoint := fmt.Sprintf("%s:%d", BindHost, port) - smtpHost, err := su.smtpd.Host(su.ctx) - require.NoError(su.T(), err) - _smtpPort, err := su.smtpd.MappedPort(su.ctx, "8025/tcp") + smtpHost := BindHost + smtpPortProto, err := su.smtpd.MappedPort(su.ctx, "8025/tcp") require.NoError(su.T(), err) - smtpPort := strings.SplitN(string(_smtpPort), "/", 2)[0] - apiEndpoint, err := su.smtpd.PortEndpoint(su.ctx, "8080/tcp", "http") + smtpPort := strings.SplitN(string(smtpPortProto), "/", 2)[0] + apiPort, err := su.smtpd.MappedPort(su.ctx, "8080/tcp") require.NoError(su.T(), err) + apiEndpoint := fmt.Sprintf("http://%s:%s", BindHost, apiPort.Port()) config := fmt.Sprintf(` smtpd-proxy: listen: %s - ehlo: localhost - username: user@example.com + ehlo: 127.0.0.1 + username: user-smtp@example.com password: password is_anon_auth_allowed: false upstream-servers: @@ -82,17 +82,18 @@ smtpd-proxy: RunMainWithConfig(su.ctx, su.T(), config, port, func(t *testing.T, conn net.Conn) { fromEmail := "" // Setup authentication information. - auth := smtp.PlainAuth("", "user@example.com", "password", BindHost) - to := []string{"recipient@example.net"} + auth := smtp.PlainAuth("", "user-smtp@example.com", "password", BindHost) + to := []string{"recipient-smtp@example.net"} msg := strings.Join([]string{ "To: ", "From: " + fromEmail, - "Subject: Test E-mail!", + "Subject: Test E-mail! )SMTP)", "", "This is the email body (SMTP).", "", }, "\r\n") - err := smtp.SendMail(proxyEndpoint, auth, "sender@example.org", to, []byte(msg)) + // FakeSMTPServer is not very stable so it seems to be a good idea to retry + err := smtp.SendMail(proxyEndpoint, auth, "sender-smtp@example.org", to, []byte(msg)) require.NoError(t, err) smtpFakerReceived := requireFakerReceivedEmailWithContains(t, apiEndpoint, "gotest-simple-smtp@esmtp.email") @@ -109,13 +110,14 @@ func (su *SMTPSystemTestSuite) TestSmokeSMTPForwardAcceptsEMailWithAttachments() _smtpPort, err := su.smtpd.MappedPort(su.ctx, "8025/tcp") require.NoError(su.T(), err) smtpPort := strings.SplitN(string(_smtpPort), "/", 2)[0] - apiEndpoint, err := su.smtpd.PortEndpoint(su.ctx, "8080/tcp", "http") + apiPort, err := su.smtpd.MappedPort(su.ctx, "8080/tcp") require.NoError(su.T(), err) + apiEndpoint := fmt.Sprintf("http://%s:%s", BindHost, apiPort.Port()) config := fmt.Sprintf(` smtpd-proxy: listen: %s - ehlo: localhost - username: user@example.com + ehlo: 127.0.0.1 + username: user-smtp@example.com password: password is_anon_auth_allowed: false upstream-servers: @@ -127,16 +129,23 @@ smtpd-proxy: `, proxyEndpoint, smtpHost, smtpPort, smtpHost) RunMainWithConfig(su.ctx, su.T(), config, port, func(t *testing.T, conn net.Conn) { // Setup authentication information. - auth := smtp.PlainAuth("", "user@example.com", "password", BindHost) + auth := smtp.PlainAuth("", "user-smtp@example.com", "password", BindHost) envelope := email.NewEmail() - envelope.To = []string{""} - envelope.From = "" - envelope.Subject = "Subject: Test E-mail!" + envelope.To = []string{""} + envelope.From = "" + envelope.Subject = "Subject: Test SMTP E-mail!" envelope.Text = []byte("This is the email body (SMTP).") - envelope.Sender = "recipient@example.net" + envelope.Sender = "recipient-smtp@example.net" _, err := envelope.AttachFile("_testData/text-attachment.txt") require.NoError(t, err, "failed to attach file") - err = envelope.Send(proxyEndpoint, auth) + // FakeSMTPServer is not very stable so it seems to be a good idea to retry + for i := 0; i < 3; i++ { + err = envelope.Send(proxyEndpoint, auth) + if err == nil { + break + } + time.Sleep(time.Millisecond * time.Duration((50 + i*25))) + } require.NoError(t, err, "failed to send message") fakerEmailReceived := requireFakerReceivedEmailWithContains(t, apiEndpoint, envelope.From) @@ -169,18 +178,19 @@ func initFakeSMTPContainer(ctx context.Context) (container tc.Container, err err } type fakerAPIContentItem struct { - ID int `json:"id"` - FromAddress string `json:"fromAddress"` - ToAddress string `json:"toAddress"` - Subject string `json:"subject"` - ReceivedOn time.Time `json:"receivedOn"` - RawData string `json:"rawData"` + ID int `json:"id"` + FromAddress string `json:"fromAddress"` + ToAddress string `json:"toAddress"` + Subject string `json:"subject"` + ReceivedOn string `json:"receivedOn"` + RawData string `json:"rawData"` Attachments []struct { ID int `json:"id"` Filename string `json:"filename"` Data string `json:"data"` } `json:"attachments"` } + type fakerAPIEmail struct { Content []fakerAPIContentItem `json:"content"` Page struct { @@ -226,8 +236,8 @@ func requireFakerReceivedEmailWithContains(t *testing.T, fakerAPIBaseURL, needle } return forwardedEmail != nil }, - 5*time.Second, - 50*time.Millisecond, + 10*time.Second, + 300*time.Millisecond, "Failed to obtain ses payloads for %s", needle, ) require.NotNil(t, forwardedEmail) diff --git a/tests/system_auth_test.go b/tests/system_auth_test.go index e32593b..a9c716b 100644 --- a/tests/system_auth_test.go +++ b/tests/system_auth_test.go @@ -32,7 +32,7 @@ func TestSmokeAuthCredentials(t *testing.T) { config := fmt.Sprintf(` smtpd-proxy: listen: %s - ehlo: localhost + ehlo: 127.0.0.1 username: user@example.com password: password is_anon_auth_allowed: false @@ -81,7 +81,7 @@ func TestSmokeAnonCredentialsOk(t *testing.T) { config := fmt.Sprintf(` smtpd-proxy: listen: %s - ehlo: localhost + ehlo: 127.0.0.1 is_anon_auth_allowed: true upstream-servers: - type: log