2023-08-23 16:17:01 +01:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2023-09-11 01:33:08 +01:00
|
|
|
"bufio"
|
2023-08-23 16:17:01 +01:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
postfixLookup "github.com/1f349/lotus/postfix-lookup"
|
2023-09-11 01:33:08 +01:00
|
|
|
"github.com/1f349/lotus/sendmail"
|
2023-11-19 23:36:45 +00:00
|
|
|
"github.com/1f349/mjwt/auth"
|
|
|
|
"github.com/1f349/mjwt/claims"
|
2023-09-11 01:33:08 +01:00
|
|
|
"github.com/emersion/go-message/mail"
|
2023-08-23 16:17:01 +01:00
|
|
|
"github.com/golang-jwt/jwt/v4"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"slices"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
defaultPostfixLookup = func(key string) (string, error) {
|
|
|
|
switch key {
|
|
|
|
case "noreply@example.com", "admin@example.com":
|
|
|
|
return "admin@example.com", nil
|
|
|
|
case "user@example.com":
|
|
|
|
return "user@example.com", nil
|
|
|
|
}
|
|
|
|
return "", postfixLookup.ErrInvalidAlias
|
|
|
|
}
|
|
|
|
timeNow = func() time.Time {
|
|
|
|
return time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type fakeSmtp struct {
|
2023-09-11 01:33:08 +01:00
|
|
|
from *mail.Address
|
|
|
|
body []byte
|
2023-08-23 16:17:01 +01:00
|
|
|
}
|
|
|
|
|
2023-09-11 01:33:08 +01:00
|
|
|
func (f *fakeSmtp) Send(mail *sendmail.Mail) error {
|
|
|
|
// remove the Bcc header line
|
|
|
|
s := bufio.NewScanner(bytes.NewReader(mail.Body))
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
b.Grow(len(mail.Body))
|
|
|
|
for s.Scan() {
|
|
|
|
a := s.Text()
|
|
|
|
if strings.HasPrefix(a, "Bcc:") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
b.WriteString(a + "\r\n")
|
|
|
|
}
|
|
|
|
if err := s.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// check values are the same
|
|
|
|
if mail.From.String() != f.from.String() {
|
2023-08-23 16:17:01 +01:00
|
|
|
return fmt.Errorf("test fail: invalid from address")
|
|
|
|
}
|
2023-09-11 01:33:08 +01:00
|
|
|
if !slices.Equal(b.Bytes(), f.body) {
|
2023-08-23 16:17:01 +01:00
|
|
|
return fmt.Errorf("test fail: invalid message body")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type fakeFailedSmtp struct{}
|
|
|
|
|
2023-09-11 01:33:08 +01:00
|
|
|
func (f *fakeFailedSmtp) Send(mail *sendmail.Mail) error {
|
2023-08-23 16:17:01 +01:00
|
|
|
return errors.New("sending failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
var messageSenderTestData = []struct {
|
|
|
|
req func() (*http.Request, error)
|
|
|
|
smtp Smtp
|
|
|
|
claims AuthClaims
|
|
|
|
status int
|
|
|
|
output string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", nil)
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Missing request body",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", strings.NewReader(`{`))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Invalid JSON body",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 16:17:01 +01:00
|
|
|
From: "noreply2@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "plain",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Invalid sender alias",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 16:17:01 +01:00
|
|
|
From: "user@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "plain",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "User does not own sender alias",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 16:17:01 +01:00
|
|
|
From: "noreply@example.com, user2@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "plain",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
2023-08-23 17:36:23 +01:00
|
|
|
output: "Invalid mail message: multiple from addresses",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 17:36:23 +01:00
|
|
|
From: "noreply@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "no",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Invalid mail message: invalid body type",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 17:36:23 +01:00
|
|
|
From: "noreply@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "a <user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "no",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Invalid mail message: mail: unclosed angle-addr",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 17:36:23 +01:00
|
|
|
From: "noreply@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "a <user>",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "no",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusBadRequest,
|
|
|
|
output: "Invalid mail message: mail: missing @ in addr-spec",
|
2023-08-23 16:17:01 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 16:17:01 +01:00
|
|
|
From: "noreply@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "plain",
|
|
|
|
Body: "Plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeFailedSmtp{},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusInternalServerError,
|
|
|
|
output: "Failed to send mail",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
req: func() (*http.Request, error) {
|
2023-09-11 01:33:08 +01:00
|
|
|
j, err := json.Marshal(sendmail.Json{
|
2023-08-23 16:17:01 +01:00
|
|
|
From: "noreply@example.com",
|
|
|
|
ReplyTo: "admin@example.com",
|
|
|
|
To: "user@example.com",
|
|
|
|
Cc: "user2@example.com",
|
|
|
|
Bcc: "user3@example.com",
|
|
|
|
Subject: "Test Subject",
|
|
|
|
BodyType: "plain",
|
|
|
|
Body: "Some plain text",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j))
|
|
|
|
},
|
|
|
|
smtp: &fakeSmtp{
|
2023-09-11 01:33:08 +01:00
|
|
|
from: &mail.Address{Address: "noreply@example.com"},
|
2023-08-23 16:17:01 +01:00
|
|
|
body: []byte("Mime-Version: 1.0\r\n" +
|
|
|
|
"Content-Type: text/plain; charset=utf-8\r\n" +
|
|
|
|
"Cc: <user2@example.com>\r\n" +
|
|
|
|
"To: <user@example.com>\r\n" +
|
|
|
|
"Reply-To: <admin@example.com>\r\n" +
|
|
|
|
"From: <noreply@example.com>\r\n" +
|
|
|
|
"Subject: Test Subject\r\n" +
|
|
|
|
"Date: Sat, 01 Jan 2000 00:00:00 +0000\r\n" +
|
|
|
|
"\r\n" +
|
2023-09-11 01:33:08 +01:00
|
|
|
"Some plain text\r\n"),
|
2023-08-23 16:17:01 +01:00
|
|
|
},
|
|
|
|
claims: makeFakeAuthClaims("admin@example.com"),
|
|
|
|
status: http.StatusAccepted,
|
|
|
|
output: "",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeFakeAuthClaims(subject string) AuthClaims {
|
|
|
|
return struct {
|
|
|
|
jwt.RegisteredClaims
|
|
|
|
ClaimType string
|
|
|
|
Claims auth.AccessTokenClaims
|
|
|
|
}{
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
Issuer: "Test",
|
|
|
|
Subject: subject,
|
|
|
|
Audience: jwt.ClaimStrings{"mail.example.com"},
|
|
|
|
},
|
|
|
|
ClaimType: "access-token",
|
|
|
|
Claims: auth.AccessTokenClaims{Perms: claims.NewPermStorage()},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestMessageSender(t *testing.T) {
|
|
|
|
for _, i := range messageSenderTestData {
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
req, err := i.req()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
MessageSender(i.smtp)(rec, req, httprouter.Params{}, i.claims)
|
|
|
|
|
|
|
|
res := rec.Result()
|
|
|
|
assert.Equal(t, i.status, res.StatusCode)
|
|
|
|
assert.NotNil(t, res.Body)
|
|
|
|
all, err := io.ReadAll(res.Body)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
if i.output == "" {
|
|
|
|
assert.Equal(t, "", string(all))
|
|
|
|
} else {
|
|
|
|
assert.Equal(t, "{\"error\":\""+i.output+"\"}\n", string(all))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|