listmonk/internal/messenger/email/email.go
Kailash Nadh 68afd61024 Add support for alternate plaintext body for e-mails.
This commit removes the Go html2text lib that would automatically
convert all HTML messages to plaintext and add them as the alt
text body to outgoing e-mails. This lib also had memory leak
issues with certain kinds of HTML templates.

A new UI field for optionally adding an alt plaintext body to
a campaign is added. On enabling, it converts the HTML message in
the campaign editor into plaintext (using the textversionjs lib).

This introduces breaking changes in the campaigns table schema,
model, and template compilation.
2021-01-30 18:49:47 +05:30

159 lines
3.3 KiB
Go

package email
import (
"crypto/tls"
"fmt"
"math/rand"
"net/smtp"
"net/textproto"
"github.com/knadh/listmonk/internal/messenger"
"github.com/knadh/smtppool"
)
const emName = "email"
// Server represents an SMTP server's credentials.
type Server struct {
Username string `json:"username"`
Password string `json:"password"`
AuthProtocol string `json:"auth_protocol"`
TLSEnabled bool `json:"tls_enabled"`
TLSSkipVerify bool `json:"tls_skip_verify"`
EmailHeaders map[string]string `json:"email_headers"`
// Rest of the options are embedded directly from the smtppool lib.
// The JSON tag is for config unmarshal to work.
smtppool.Opt `json:",squash"`
pool *smtppool.Pool
}
// Emailer is the SMTP e-mail messenger.
type Emailer struct {
servers []*Server
}
// New returns an SMTP e-mail Messenger backend with a the given SMTP servers.
func New(servers ...Server) (*Emailer, error) {
e := &Emailer{
servers: make([]*Server, 0, len(servers)),
}
for _, srv := range servers {
s := srv
var auth smtp.Auth
switch s.AuthProtocol {
case "cram":
auth = smtp.CRAMMD5Auth(s.Username, s.Password)
case "plain":
auth = smtp.PlainAuth("", s.Username, s.Password, s.Host)
case "login":
auth = &smtppool.LoginAuth{Username: s.Username, Password: s.Password}
case "", "none":
default:
return nil, fmt.Errorf("unknown SMTP auth type '%s'", s.AuthProtocol)
}
s.Opt.Auth = auth
// TLS config.
if s.TLSEnabled {
s.TLSConfig = &tls.Config{}
if s.TLSSkipVerify {
s.TLSConfig.InsecureSkipVerify = s.TLSSkipVerify
} else {
s.TLSConfig.ServerName = s.Host
}
}
pool, err := smtppool.New(s.Opt)
if err != nil {
return nil, err
}
s.pool = pool
e.servers = append(e.servers, &s)
}
return e, nil
}
// Name returns the Server's name.
func (e *Emailer) Name() string {
return emName
}
// Push pushes a message to the server.
func (e *Emailer) Push(m messenger.Message) error {
// If there are more than one SMTP servers, send to a random
// one from the list.
var (
ln = len(e.servers)
srv *Server
)
if ln > 1 {
srv = e.servers[rand.Intn(ln)]
} else {
srv = e.servers[0]
}
// Are there attachments?
var files []smtppool.Attachment
if m.Attachments != nil {
files = make([]smtppool.Attachment, 0, len(m.Attachments))
for _, f := range m.Attachments {
a := smtppool.Attachment{
Filename: f.Name,
Header: f.Header,
Content: make([]byte, len(f.Content)),
}
copy(a.Content, f.Content)
files = append(files, a)
}
}
em := smtppool.Email{
From: m.From,
To: m.To,
Subject: m.Subject,
Attachments: files,
}
em.Headers = textproto.MIMEHeader{}
// Attach e-mail level headers.
if len(m.Headers) > 0 {
em.Headers = m.Headers
}
// Attach SMTP level headers.
if len(srv.EmailHeaders) > 0 {
for k, v := range srv.EmailHeaders {
em.Headers.Set(k, v)
}
}
switch m.ContentType {
case "plain":
em.Text = []byte(m.Body)
default:
em.HTML = m.Body
if len(m.AltBody) > 0 {
em.Text = m.AltBody
}
}
return srv.pool.Send(em)
}
// Flush flushes the message queue to the server.
func (e *Emailer) Flush() error {
return nil
}
// Close closes the SMTP pools.
func (e *Emailer) Close() error {
for _, s := range e.servers {
s.pool.Close()
}
return nil
}