package email import ( "crypto/tls" "fmt" "math/rand" "net/smtp" "net/textproto" "strings" "github.com/knadh/listmonk/models" "github.com/knadh/smtppool" ) const ( emName = "email" hdrReturnPath = "Return-Path" hdrBcc = "Bcc" hdrCc = "Cc" ) // Server represents an SMTP server's credentials. type Server struct { Username string `json:"username"` Password string `json:"password"` AuthProtocol string `json:"auth_protocol"` TLSType string `json:"tls_type"` 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 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.TLSType != "none" { s.TLSConfig = &tls.Config{} if s.TLSSkipVerify { s.TLSConfig.InsecureSkipVerify = s.TLSSkipVerify } else { s.TLSConfig.ServerName = s.Host } // SSL/TLS, not STARTTLS. if s.TLSType == "TLS" { s.Opt.SSL = true } } 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 models.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 SMTP level headers. for k, v := range srv.EmailHeaders { em.Headers.Set(k, v) } // Attach e-mail level headers. for k, v := range m.Headers { em.Headers.Set(k, v[0]) } // If the `Return-Path` header is set, it should be set as the // the SMTP envelope sender (via the Sender field of the email struct). if sender := em.Headers.Get(hdrReturnPath); sender != "" { em.Sender = sender em.Headers.Del(hdrReturnPath) } // If the `Bcc` header is set, it should be set on the Envelope if bcc := em.Headers.Get(hdrBcc); bcc != "" { for _, part := range strings.Split(bcc, ",") { em.Bcc = append(em.Bcc, strings.TrimSpace(part)) } em.Headers.Del(hdrBcc) } // If the `Cc` header is set, it should be set on the Envelope if cc := em.Headers.Get(hdrCc); cc != "" { for _, part := range strings.Split(cc, ",") { em.Cc = append(em.Cc, strings.TrimSpace(part)) } em.Headers.Del(hdrCc) } 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 }