mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The "sending mail" Info log includes the subject line, which for DM and mention notifications contains the sender's name and other PII visible to anyone with log access. The "to" address is enough operational signal to confirm that a notification fired.
379 lines
9.6 KiB
Go
379 lines
9.6 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package mail
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net"
|
|
"net/mail"
|
|
"net/smtp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.sajari.com/docconv/v2"
|
|
"github.com/pkg/errors"
|
|
gomail "gopkg.in/mail.v2"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
const (
|
|
TLS = "TLS"
|
|
StartTLS = "STARTTLS"
|
|
)
|
|
|
|
type SMTPConfig struct {
|
|
ConnectionSecurity string
|
|
SkipServerCertificateVerification bool
|
|
Hostname string
|
|
ServerName string
|
|
Server string
|
|
Port string
|
|
ServerTimeout int
|
|
Username string
|
|
Password string
|
|
EnableSMTPAuth bool
|
|
SendEmailNotifications bool
|
|
FeedbackName string
|
|
FeedbackEmail string
|
|
ReplyToAddress string
|
|
}
|
|
|
|
type mailData struct {
|
|
mimeTo string
|
|
smtpTo string
|
|
from mail.Address
|
|
cc string
|
|
replyTo mail.Address
|
|
subject string
|
|
htmlBody string
|
|
embeddedFiles map[string]io.Reader
|
|
mimeHeaders map[string]string
|
|
messageID string
|
|
inReplyTo string
|
|
references string
|
|
category string
|
|
}
|
|
|
|
// smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
|
|
type smtpClient interface {
|
|
Mail(string) error
|
|
Rcpt(string) error
|
|
Data() (io.WriteCloser, error)
|
|
}
|
|
|
|
func encodeRFC2047Word(s string) string {
|
|
return mime.BEncoding.Encode("utf-8", s)
|
|
}
|
|
|
|
type authChooser struct {
|
|
smtp.Auth
|
|
config *SMTPConfig
|
|
}
|
|
|
|
func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|
smtpAddress := a.config.ServerName + ":" + a.config.Port
|
|
a.Auth = LoginAuth(a.config.Username, a.config.Password, smtpAddress)
|
|
if slices.Contains(server.Auth, "PLAIN") {
|
|
a.Auth = smtp.PlainAuth("", a.config.Username, a.config.Password, a.config.ServerName+":"+a.config.Port)
|
|
}
|
|
return a.Auth.Start(server)
|
|
}
|
|
|
|
type loginAuth struct {
|
|
username, password, host string
|
|
}
|
|
|
|
func LoginAuth(username, password, host string) smtp.Auth {
|
|
return &loginAuth{username, password, host}
|
|
}
|
|
|
|
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|
if !server.TLS {
|
|
return "", nil, errors.New("unencrypted connection")
|
|
}
|
|
|
|
if server.Name != a.host {
|
|
return "", nil, errors.New("wrong host name")
|
|
}
|
|
|
|
return "LOGIN", []byte{}, nil
|
|
}
|
|
|
|
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
|
if more {
|
|
switch string(fromServer) {
|
|
case "Username:":
|
|
return []byte(a.username), nil
|
|
case "Password:":
|
|
return []byte(a.password), nil
|
|
default:
|
|
return nil, errors.New("Unknown fromServer")
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func ConnectToSMTPServerAdvanced(config *SMTPConfig) (net.Conn, error) {
|
|
var conn net.Conn
|
|
var err error
|
|
|
|
smtpAddress := config.Server + ":" + config.Port
|
|
dialer := &net.Dialer{
|
|
Timeout: time.Duration(config.ServerTimeout) * time.Second,
|
|
}
|
|
|
|
if config.ConnectionSecurity == TLS {
|
|
tlsconfig := &tls.Config{
|
|
InsecureSkipVerify: config.SkipServerCertificateVerification,
|
|
ServerName: config.ServerName,
|
|
}
|
|
|
|
conn, err = tls.DialWithDialer(dialer, "tcp", smtpAddress, tlsconfig)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to connect to the SMTP server through TLS")
|
|
}
|
|
} else {
|
|
conn, err = dialer.Dial("tcp", smtpAddress)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
|
|
}
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
func ConnectToSMTPServer(config *SMTPConfig) (net.Conn, error) {
|
|
return ConnectToSMTPServerAdvanced(config)
|
|
}
|
|
|
|
func NewSMTPClientAdvanced(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
var c *smtp.Client
|
|
ec := make(chan error)
|
|
go func() {
|
|
var err error
|
|
c, err = smtp.NewClient(conn, config.ServerName+":"+config.Port)
|
|
if err != nil {
|
|
ec <- err
|
|
return
|
|
}
|
|
cancel()
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
err := ctx.Err()
|
|
if err != nil && err.Error() != "context canceled" {
|
|
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
|
|
}
|
|
case err := <-ec:
|
|
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
|
|
}
|
|
|
|
if config.Hostname != "" {
|
|
err := c.Hello(config.Hostname)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to send hello message")
|
|
}
|
|
}
|
|
|
|
if config.ConnectionSecurity == StartTLS {
|
|
tlsconfig := &tls.Config{
|
|
InsecureSkipVerify: config.SkipServerCertificateVerification,
|
|
ServerName: config.ServerName,
|
|
}
|
|
c.StartTLS(tlsconfig)
|
|
}
|
|
|
|
if config.EnableSMTPAuth {
|
|
if err := c.Auth(&authChooser{config: config}); err != nil {
|
|
return nil, errors.Wrap(err, "authentication failed")
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func NewSMTPClient(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
|
|
return NewSMTPClientAdvanced(
|
|
ctx,
|
|
conn,
|
|
config,
|
|
)
|
|
}
|
|
|
|
func TestConnection(config *SMTPConfig) error {
|
|
conn, err := ConnectToSMTPServer(config)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to connect")
|
|
}
|
|
defer conn.Close()
|
|
|
|
sec := config.ServerTimeout
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
|
|
defer cancel()
|
|
|
|
c, err := NewSMTPClient(ctx, conn, config)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to connect")
|
|
}
|
|
c.Close()
|
|
c.Quit()
|
|
|
|
return nil
|
|
}
|
|
|
|
func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail string, category string) error {
|
|
fromMail := mail.Address{Name: config.FeedbackName, Address: config.FeedbackEmail}
|
|
replyTo := mail.Address{Name: config.FeedbackName, Address: config.ReplyToAddress}
|
|
|
|
mail := mailData{
|
|
mimeTo: to,
|
|
smtpTo: to,
|
|
from: fromMail,
|
|
cc: ccMail,
|
|
replyTo: replyTo,
|
|
subject: subject,
|
|
htmlBody: htmlBody,
|
|
embeddedFiles: embeddedFiles,
|
|
messageID: messageID,
|
|
inReplyTo: inReplyTo,
|
|
references: references,
|
|
category: category,
|
|
}
|
|
|
|
return sendMailUsingConfigAdvanced(mail, config)
|
|
}
|
|
|
|
func SendMailUsingConfig(to, subject, htmlBody string, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail, category string) error {
|
|
return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, messageID, inReplyTo, references, ccMail, category)
|
|
}
|
|
|
|
// allows for sending an email with differing MIME/SMTP recipients
|
|
func sendMailUsingConfigAdvanced(mail mailData, config *SMTPConfig) error {
|
|
if config.Server == "" {
|
|
return nil
|
|
}
|
|
|
|
conn, err := ConnectToSMTPServer(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
sec := config.ServerTimeout
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
|
|
defer cancel()
|
|
|
|
c, err := NewSMTPClient(ctx, conn, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.Quit()
|
|
defer c.Close()
|
|
|
|
return sendMail(c, mail, time.Now(), config)
|
|
}
|
|
|
|
const SendGridXSMTPAPIHeader = "X-SMTPAPI"
|
|
|
|
func sendMail(c smtpClient, mail mailData, date time.Time, config *SMTPConfig) error {
|
|
mlog.Info("sending mail", mlog.String("to", mail.smtpTo))
|
|
|
|
htmlMessage := mail.htmlBody
|
|
text, _, err := docconv.ConvertHTML(strings.NewReader(htmlMessage), true)
|
|
if err != nil {
|
|
mlog.Warn("Unable to convert html body to text", mlog.Err(err))
|
|
text = ""
|
|
}
|
|
|
|
headers := map[string][]string{
|
|
"From": {mail.from.String()},
|
|
"To": {mail.mimeTo},
|
|
"Subject": {encodeRFC2047Word(mail.subject)},
|
|
"Content-Transfer-Encoding": {"8bit"},
|
|
"Auto-Submitted": {"auto-generated"},
|
|
"Precedence": {"bulk"},
|
|
}
|
|
|
|
if mail.category != "" {
|
|
sendgridHeader := fmt.Sprintf(`{"category": %q}`, mail.category)
|
|
headers[SendGridXSMTPAPIHeader] = []string{sendgridHeader}
|
|
}
|
|
|
|
if mail.replyTo.Address != "" {
|
|
headers["Reply-To"] = []string{mail.replyTo.String()}
|
|
}
|
|
|
|
if mail.cc != "" {
|
|
headers["CC"] = []string{mail.cc}
|
|
}
|
|
|
|
if mail.messageID != "" {
|
|
headers["Message-ID"] = []string{mail.messageID}
|
|
} else {
|
|
randomStringLength := 16
|
|
msgID := fmt.Sprintf("<%s-%d@%s>", model.NewRandomString(randomStringLength), time.Now().Unix(), config.Hostname)
|
|
headers["Message-ID"] = []string{msgID}
|
|
}
|
|
|
|
if mail.inReplyTo != "" {
|
|
headers["In-Reply-To"] = []string{mail.inReplyTo}
|
|
}
|
|
|
|
if mail.references != "" {
|
|
headers["References"] = []string{mail.references}
|
|
}
|
|
|
|
for k, v := range mail.mimeHeaders {
|
|
headers[k] = []string{encodeRFC2047Word(v)}
|
|
}
|
|
|
|
m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
|
|
m.SetHeaders(headers)
|
|
m.SetDateHeader("Date", date)
|
|
m.SetBody("text/plain", text)
|
|
m.AddAlternative("text/html", htmlMessage)
|
|
|
|
for name, reader := range mail.embeddedFiles {
|
|
m.EmbedReader(name, reader)
|
|
}
|
|
|
|
if err = c.Mail(mail.from.Address); err != nil {
|
|
return errors.Wrap(err, "failed to set the from address")
|
|
}
|
|
|
|
if err = c.Rcpt(mail.smtpTo); err != nil {
|
|
return errors.Wrap(err, "failed to set the to address")
|
|
}
|
|
|
|
w, err := c.Data()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to add email message data")
|
|
}
|
|
|
|
_, err = m.WriteTo(w)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to write the email message")
|
|
}
|
|
err = w.Close()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to close connection to the SMTP server")
|
|
}
|
|
|
|
return nil
|
|
}
|