dendrite/userapi/mail/mail.go

162 lines
3.6 KiB
Go

package mail
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"net/smtp"
"text/template"
"time"
"github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/userapi/api"
)
const (
messageIdByteLength = 48
)
type Mailer interface {
// Send is used in
// - https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
// - https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-register-email-requesttoken
// - https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-password-email-requesttoken
Send(*Mail, api.ThreepidSessionType) error
}
// SmtpMailer is safe for concurrent use. It will block if email sending is in progress as long as it uses single connection.
type SmtpMailer struct {
conf config.EmailConf
templates []*template.Template
auth smtp.Auth
cl *smtp.Client
// sendMutex guards ensures that MAIL, RCPT and DATA commands are not messed between mails.
sendMutex sync.Mutex
}
type Mail struct {
To string
Link string
Token string
Extra []string
}
type Substitutions struct {
*Mail
Date string
MessageId string
}
func (m *SmtpMailer) Send(mail *Mail, t api.ThreepidSessionType) error {
return m.send(mail, m.templates[t])
}
func (m *SmtpMailer) send(mail *Mail, t *template.Template) error {
messageId, err := internal.GenerateBlob(messageIdByteLength)
if err != nil {
return err
}
s := Substitutions{
Mail: mail,
Date: time.Now().Format(time.RFC1123Z),
MessageId: messageId,
}
b := bytes.Buffer{}
err = t.Execute(&b, s)
if err != nil {
return err
}
if err = validateLine(mail.To); err != nil {
return err
}
// lock at the point when data are prepared and we are executing commands
m.sendMutex.Lock()
defer m.sendMutex.Unlock()
if err = m.cl.Mail(m.conf.From); err != nil {
return err
}
if err = m.cl.Rcpt(mail.To); err != nil {
return err
}
var w io.WriteCloser
w, err = m.cl.Data()
if err != nil {
return err
}
_, err = w.Write(b.Bytes())
if err != nil {
return err
}
return w.Close()
}
func NewMailer(c *config.UserAPI) (Mailer, error) {
if err := validateLine(c.Email.From); err != nil {
return nil, err
}
sessionTypes := api.ThreepidSessionTypes()
templates := make([]*template.Template, len(sessionTypes))
for _, t := range sessionTypes {
name := t.Name()
templateRaw, err := ioutil.ReadFile(fmt.Sprintf("%s/%s.eml", c.Email.TemplatesPath, name))
if err != nil {
return nil, err
}
template, err := template.New(name).Parse(string(templateRaw))
if err != nil {
return nil, err
}
templates[t] = template
}
cl, err := smtp.Dial(c.Email.Smtp.Host)
if err != nil {
return nil, err
}
// defer c.Close() # TODO exit gracefully
if err = cl.Hello("localhost"); err != nil {
return nil, err
}
if ok, _ := cl.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.Email.Smtp.Host}
if err = cl.StartTLS(config); err != nil {
return nil, err
}
}
var auth smtp.Auth
if c.Email.Smtp.User != "" {
auth = smtp.PlainAuth(
"",
c.Email.Smtp.User,
c.Email.Smtp.Password,
c.Email.Smtp.Host,
)
if err = cl.Auth(auth); err != nil {
return nil, err
}
}
return &SmtpMailer{
conf: c.Email,
templates: templates,
auth: auth,
cl: cl,
sendMutex: sync.Mutex{},
}, nil
}
// validateLine checks to see if a line has CR or LF as per RFC 5321
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}