mattermost/server/platform/shared/mail/mail_test.go
Jesse Hallam d4fc0ecb1c
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / Check go fix (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Elasticsearch v8 Compatibility (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 0) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 1) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 2) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres FIPS Test Results (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
YAML Lint / yamllint (push) Waiting to run
MM-68150: Upgrade golangci-lint to v2.12.2 (#36554)
* Simplify invite_people email parsing

Replace backwards in-place mutation loop with a straightforward forward
filter into a new slice. Extract into parseEmailList so the logic can be
unit tested directly.

* MM-68150: Upgrade golangci-lint to v2.12.2

Remove //go:fix inline from NewPointer, which is a generic function not
yet supported by the inline analyzer, and fix 11 slicesbackward
modernize issues flagged by the new version.

* MM-68150: Enable all linters by default; disable those with >20 existing issues

Switch from opt-in (default: none) to opt-out (default: all) so new
linters added to golangci-lint are evaluated automatically. Explicitly
disable every linter that has more than 20 pre-existing violations,
deferring those for later cleanup. Also disable a handful of linters
whose violations are intentional patterns in this codebase (nilerr,
dogsled, sqlclosecheck, iotamixing, predeclared, containedctx, iface,
gocheckcompilerdirectives, promlinter, goprintffuncname, gomoddirectives).

* MM-68150: Fix mirror linter issues

Replace Write([]byte(s)) with WriteString(s), and FindIndex([]byte(s))
with FindStringIndex(s), to avoid unnecessary allocations.

* MM-68150: Fix nosprintfhostport linter issue

Use net.JoinHostPort to construct host:port strings instead of
fmt.Sprintf with a manually formatted pattern.

* MM-68150: Fix rowserrcheck and sqlclosecheck linter issues

Check rows.Err() after iteration loops in schema_dump.go. In the
sqlx_wrapper test, defer rows.Close() rather than closing inline.

* MM-68150: Fix nilnesserr linter issues — wrong variable in error handlers

In 11 places, a stale variable (often the outer err from a prior
assignment) was used instead of the freshly-checked error variable
(appErr, rowErr, jsonErr, writeErr, esErr). Each produces a typed-nil
wrapped in a non-nil interface, silently discarding the real error.

* MM-68150: Add i18n string for app.compile_csv_chunks.write_error

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-05-14 17:29:37 -04:00

480 lines
14 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mail
import (
"bytes"
"context"
"io"
"net"
"net/mail"
"net/smtp"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func getConfig() *SMTPConfig {
server := os.Getenv("MM_EMAILSETTINGS_SMTPSERVER")
if server == "" {
server = "localhost"
}
port := os.Getenv("MM_EMAILSETTINGS_SMTPPORT")
if port == "" {
port = "10025"
}
return &SMTPConfig{
ConnectionSecurity: "",
SkipServerCertificateVerification: false,
Hostname: "localhost",
ServerName: server,
Server: server,
Port: port,
ServerTimeout: 10,
Username: "",
Password: "",
EnableSMTPAuth: false,
SendEmailNotifications: true,
FeedbackName: "",
FeedbackEmail: "test@example.com",
ReplyToAddress: "test@example.com",
}
}
func TestMailConnectionFromConfig(t *testing.T) {
cfg := getConfig()
conn, err := ConnectToSMTPServer(cfg)
require.NoError(t, err, "Should connect to the SMTP Server %v", err)
_, err = NewSMTPClient(context.Background(), conn, cfg)
require.NoError(t, err, "Should get new SMTP client")
cfg.Server = "wrongServer"
cfg.Port = "553"
_, err = ConnectToSMTPServer(cfg)
require.Error(t, err, "Should not connect to the SMTP Server")
}
func TestMailConnectionAdvanced(t *testing.T) {
cfg := getConfig()
conn, err := ConnectToSMTPServerAdvanced(cfg)
require.NoError(t, err, "Should connect to the SMTP Server")
defer conn.Close()
_, err2 := NewSMTPClientAdvanced(context.Background(), conn, cfg)
require.NoError(t, err2, "Should get new SMTP client")
l, err3 := net.Listen("tcp", "localhost:") // emulate nc -l <random-port>
require.NoError(t, err3, "Should've open a network socket and listen")
defer l.Close()
cfg = getConfig()
cfg.Server = strings.Split(l.Addr().String(), ":")[0]
cfg.Port = strings.Split(l.Addr().String(), ":")[1]
cfg.ServerTimeout = 1
conn2, err := ConnectToSMTPServerAdvanced(cfg)
require.NoError(t, err, "Should connect to the SMTP Server")
defer conn2.Close()
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
cfg = getConfig()
cfg.Server = strings.Split(l.Addr().String(), ":")[0]
cfg.Port = strings.Split(l.Addr().String(), ":")[1]
cfg.ServerTimeout = 1
_, err4 := NewSMTPClientAdvanced(
ctx,
conn2,
cfg,
)
require.Error(t, err4, "Should get a timeout get while creating a new SMTP client")
assert.Contains(t, err4.Error(), "unable to connect to the SMTP server")
cfg = getConfig()
cfg.Server = "wrongServer"
cfg.Port = "553"
cfg.ServerTimeout = 1
_, err5 := ConnectToSMTPServerAdvanced(cfg)
require.Error(t, err5, "Should not connect to the SMTP Server")
}
func TestSendMailUsingConfig(t *testing.T) {
cfg := getConfig()
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
err2 := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true, "", "", "", emailCC, "")
require.NoError(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket
err3 := RetryInbucket(5, func() error {
var err error
resultsMailbox, err = GetMailBox(emailTo)
return err
})
if err3 != nil {
t.Log(err3)
t.Log("No email was received, maybe due load on the server. Skipping this verification")
} else {
if len(resultsMailbox) > 0 {
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
resultsEmail, err := GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
require.NoError(t, err, "Could not get message from mailbox")
require.Contains(t, emailBody, resultsEmail.Body.Text, "Wrong received message %s", resultsEmail.Body.Text)
}
}
}
func TestSendMailPlainText(t *testing.T) {
cfg := getConfig()
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailCC = "test@example.com"
tests := []struct {
name string
emailBodyHTML string
expectedBodyText string
}{
{
name: "Heading",
emailBodyHTML: "<h1>This is a test from autobot</h1><h2>This is a subheading</h2>",
expectedBodyText: "***************************\nThis is a test from autobot\n***************************\n\n--------------------\nThis is a subheading\n--------------------",
},
{
name: "List",
emailBodyHTML: "<ul><li>Item 1</li><li>Item 2</li></ul>",
expectedBodyText: "* Item 1\n* Item 2",
},
{
name: "Inline formatting",
emailBodyHTML: "<p><strong>Strong</strong> and <a href='https://example.com'>link</a>",
expectedBodyText: "*Strong* and link ( https://example.com )",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
DeleteMailBox(emailTo)
err := SendMailUsingConfig(emailTo, emailSubject, test.emailBodyHTML, cfg, true, "", "", "", emailCC, "")
require.NoError(t, err, "Should connect to the SMTP Server")
var resultsMailbox JSONMessageHeaderInbucket
err = RetryInbucket(5, func() error {
var err2 error
resultsMailbox, err2 = GetMailBox(emailTo)
return err2
})
if err != nil {
t.Log("No email was received, maybe due load on the server. Failing this test")
t.Error(err)
} else {
require.NotEmpty(t, resultsMailbox, "Mailbox should contain at least one message")
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
resultsEmail, err := GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
require.NoError(t, err, "Could not get message from mailbox")
require.Contains(t, test.emailBodyHTML, resultsEmail.Body.HTML, "Wrong received message %s", resultsEmail.Body.Text)
require.Contains(t, resultsEmail.Body.Text, test.expectedBodyText, "Wrong message plain text conversion %s", resultsEmail.Body.Text)
}
})
}
}
func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) {
cfg := getConfig()
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
embeddedFiles := map[string]io.Reader{
"test1.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
"test2.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
}
err2 := SendMailWithEmbeddedFilesUsingConfig(emailTo, emailSubject, emailBody, embeddedFiles, cfg, true, "", "", "", emailCC, "")
require.NoError(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket
err3 := RetryInbucket(5, func() error {
var err error
resultsMailbox, err = GetMailBox(emailTo)
return err
})
if err3 != nil {
t.Log(err3)
t.Log("No email was received, maybe due load on the server. Skipping this verification")
} else {
if len(resultsMailbox) > 0 {
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
resultsEmail, err := GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
require.NoError(t, err, "Could not get message from mailbox")
require.Contains(t, emailBody, resultsEmail.Body.Text, "Wrong received message %s", resultsEmail.Body.Text)
// Usign the message size because the inbucket API doesn't return embedded attachments through the API
require.Greater(t, resultsEmail.Size, 1500, "the file size should be more because the embedded attachments")
}
}
}
func TestSendMailUsingConfigAdvanced(t *testing.T) {
cfg := getConfig()
//Delete all the messages before check the sample email
DeleteMailBox("test2@example.com")
// create two files with the same name that will both be attached to the email
file1, err := os.CreateTemp("", "*")
require.NoError(t, err)
defer os.Remove(file1.Name())
file1.WriteString("hello world")
file1.Close()
file2, err := os.CreateTemp("", "*")
require.NoError(t, err)
defer os.Remove(file2.Name())
file2.WriteString("foo bar")
file2.Close()
embeddedFiles := map[string]io.Reader{
"test": bytes.NewReader([]byte("test data")),
}
headers := make(map[string]string)
headers["TestHeader"] = "TestValue"
mail := mailData{
mimeTo: "test@example.com",
smtpTo: "test2@example.com",
from: mail.Address{Name: "Nobody", Address: "nobody@mattermost.com"},
replyTo: mail.Address{Name: "ReplyTo", Address: "reply_to@mattermost.com"},
subject: "Testing this email",
htmlBody: "This is a test from autobot",
embeddedFiles: embeddedFiles,
mimeHeaders: headers,
}
err = sendMailUsingConfigAdvanced(mail, cfg)
require.NoError(t, err, "Should connect to the SMTP Server: %v", err)
//Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket
err = RetryInbucket(5, func() error {
var mailErr error
resultsMailbox, mailErr = GetMailBox(mail.smtpTo)
return mailErr
})
require.NoError(t, err, "No emails found for address %s. error: %v", mail.smtpTo, err)
require.NotEqual(t, len(resultsMailbox), 0)
require.Contains(t, resultsMailbox[0].To[0], mail.mimeTo, "Wrong To recipient")
resultsEmail, err := GetMessageFromMailbox(mail.smtpTo, resultsMailbox[0].ID)
require.NoError(t, err)
require.Contains(t, mail.htmlBody, resultsEmail.Body.Text, "Wrong received message")
// verify that the To header of the email message is set to the MIME recipient, even though we got it out of the SMTP recipient's email inbox
assert.Equal(t, mail.mimeTo, resultsEmail.Header["To"][0])
// verify that the MIME from address is correct - unfortunately, we can't verify the SMTP from address
assert.Equal(t, mail.from.String(), resultsEmail.Header["From"][0])
// check that the custom mime headers came through - header case seems to get mutated
assert.Equal(t, "TestValue", resultsEmail.Header["Testheader"][0])
}
func TestAuthMethods(t *testing.T) {
auth := &authChooser{
config: &SMTPConfig{
Username: "test",
Password: "fakepass",
ServerName: "fakeserver",
Server: "fakeserver",
Port: "25",
},
}
tests := []struct {
desc string
server *smtp.ServerInfo
err string
}{
{
desc: "auth PLAIN success",
server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: true},
},
{
desc: "auth PLAIN unencrypted connection fail",
server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: false},
err: "unencrypted connection",
},
{
desc: "auth PLAIN wrong host name",
server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"PLAIN"}, TLS: true},
err: "wrong host name",
},
{
desc: "auth LOGIN success",
server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: true},
},
{
desc: "auth LOGIN unencrypted connection fail",
server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"LOGIN"}, TLS: true},
err: "wrong host name",
},
{
desc: "auth LOGIN wrong host name",
server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: false},
err: "unencrypted connection",
},
}
for i, test := range tests {
t.Run(test.desc, func(t *testing.T) {
_, _, err := auth.Start(test.server)
got := ""
if err != nil {
got = err.Error()
}
assert.True(t, got == test.err, "%d. got error = %q; want %q", i, got, test.err)
})
}
}
type mockMailer struct {
data []byte
}
func (m *mockMailer) Mail(string) error { return nil }
func (m *mockMailer) Rcpt(string) error { return nil }
func (m *mockMailer) Data() (io.WriteCloser, error) { return m, nil }
func (m *mockMailer) Write(p []byte) (int, error) {
m.data = append(m.data, p...)
return len(p), nil
}
func (m *mockMailer) Close() error { return nil }
func TestSendMail(t *testing.T) {
dir, err := os.MkdirTemp(".", "mail-test-")
require.NoError(t, err)
defer os.RemoveAll(dir)
mocm := &mockMailer{}
testCases := map[string]struct {
replyTo mail.Address
messageID string
inReplyTo string
references string
contains string
notContains string
}{
"adds reply-to header": {
mail.Address{Address: "foo@test.com"},
"",
"",
"",
"\r\nReply-To: <foo@test.com>\r\n",
"",
},
"doesn't add reply-to header": {
mail.Address{},
"",
"",
"",
"",
"\r\nReply-To:",
},
"adds message-id header": {
mail.Address{},
"<abc123@mattermost.com>",
"",
"",
"\r\nMessage-ID: <abc123@mattermost.com>\r\n",
"",
},
"always adds message-id header": {
mail.Address{},
"",
"",
"",
"\r\nMessage-ID: <",
"",
},
"adds in-reply-to header": {
mail.Address{},
"",
"<defg456@mattermost.com>",
"",
"\r\nIn-Reply-To: <defg456@mattermost.com>\r\n",
"",
},
"doesn't add in-reply-to header": {
mail.Address{},
"",
"",
"",
"",
"\r\nIn-Reply-To:",
},
"adds references header": {
mail.Address{},
"",
"",
"<ghi789@mattermost.com>",
"\r\nReferences: <ghi789@mattermost.com>\r\n",
"",
},
"doesn't add references header": {
mail.Address{},
"",
"",
"",
"",
"\r\nReferences:",
},
}
for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
mail := mailData{"", "", mail.Address{}, "", tc.replyTo, "", "", nil, nil, tc.messageID, tc.inReplyTo, tc.references, ""}
cfg := getConfig()
err = sendMail(mocm, mail, time.Now(), cfg)
require.NoError(t, err)
if tc.contains != "" {
require.Contains(t, string(mocm.data), tc.contains)
}
if tc.notContains != "" {
require.NotContains(t, string(mocm.data), tc.notContains)
}
mocm.data = []byte{}
})
}
}