diff --git a/config.toml.sample b/config.toml.sample index d186407..ddb2d1c 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -91,8 +91,7 @@ max_idle = 10 username = "xxxxx" password = "" - # Optional. Inform listmonk on which email format to use - # {html|plain|both} default: both + # Format to send e-mails in: html|plain|both. email_format = "both" # Optional. Some SMTP servers require a FQDN in the hostname. @@ -100,12 +99,19 @@ max_idle = 10 # hostname should be used. hello_hostname = "" - # Maximum time (milliseconds) to wait per e-mail push. - send_timeout = 5000 - # Maximum concurrent connections to the SMTP server. max_conns = 10 + # Time to wait for new activity on a connection before closing + # it and removing it from the pool. + idle_timeout = "15s" + + # Message send / wait timeout. + wait_timeout = "5s" + + # The number of times a message should be retried if sending fails. + max_msg_retries = 2 + [smtp.postal] enabled = false host = "my.smtp.server2" @@ -116,16 +122,27 @@ max_idle = 10 username = "xxxxx" password = "" - # Optional. Inform listmonk on which email format to use - # {html|plain|both} default: both + # Format to send e-mails in: html|plain|both. email_format = "both" - # Maximum time (milliseconds) to wait per e-mail push. - send_timeout = 5000 + # Optional. Some SMTP servers require a FQDN in the hostname. + # By default, HELLOs go with "localhost". Set this if a custom + # hostname should be used. + hello_hostname = "" # Maximum concurrent connections to the SMTP server. max_conns = 10 + # Time to wait for new activity on a connection before closing + # it and removing it from the pool. + idle_timeout = "15s" + + # Message send / wait timeout. + wait_timeout = "5s" + + # The number of times a message should be retried if sending fails. + max_msg_retries = 2 + [upload] # File storage backend. "filesystem" or "s3". diff --git a/go.mod b/go.mod index 29827cb..fdf231b 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,14 @@ module github.com/knadh/listmonk +go 1.13 require ( github.com/disintegration/imaging v1.6.2 github.com/gofrs/uuid v3.2.0+incompatible github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 github.com/jmoiron/sqlx v1.2.0 - github.com/jordan-wright/email v0.0.0-20200307200233-de844847de93 github.com/knadh/goyesql/v2 v2.1.1 github.com/knadh/koanf v0.8.1 + github.com/knadh/smtppool v0.2.0 github.com/knadh/stuffbin v1.1.0 github.com/labstack/echo v3.3.10+incompatible github.com/labstack/gommon v0.3.0 // indirect @@ -22,6 +23,3 @@ require ( jaytaylor.com/html2text v0.0.0-20200220170450-61d9dc4d7195 ) -replace github.com/jordan-wright/email => github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8 - -go 1.13 diff --git a/go.sum b/go.sum index 00e759a..e188b54 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,10 @@ github.com/knadh/goyesql/v2 v2.1.1 h1:Orp5ldaxPM4ozKHfu1m7p6iolJFXDGOpF3/jyOgO6l github.com/knadh/goyesql/v2 v2.1.1/go.mod h1:pMzCA130/ZhEIoMmSmbEFXor3A2dxl5L+JllAc/l64s= github.com/knadh/koanf v0.8.1 h1:4VLACWqrkWRQIup3ooq6lOnaSbOJSNO+YVXnJn/NPZ8= github.com/knadh/koanf v0.8.1/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w= +github.com/knadh/smtppool v0.1.1 h1:pSi1Gc5TXOaN/Z/YiqfZbk/vd9dqzXzAfQiss0QSGQU= +github.com/knadh/smtppool v0.1.1/go.mod h1:3DJHouXAgPDBz0kC50HukOsdapYSwIEfJGwuip46oCA= +github.com/knadh/smtppool v0.2.0 h1:+llTWRljNIVg05MMu9TiefELTNwblexjsd1ALAPXZUs= +github.com/knadh/smtppool v0.2.0/go.mod h1:3DJHouXAgPDBz0kC50HukOsdapYSwIEfJGwuip46oCA= github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M= github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4= github.com/knadh/stuffbin v1.1.0 h1:f5S5BHzZALjuJEgTIOMC9NidEnBJM7Ze6Lu1GHR/lwU= diff --git a/init.go b/init.go index fa0cd96..87efe38 100644 --- a/init.go +++ b/init.go @@ -7,11 +7,11 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/jmoiron/sqlx" "github.com/knadh/goyesql/v2" goyesqlx "github.com/knadh/goyesql/v2/sqlx" + "github.com/knadh/koanf" "github.com/knadh/koanf/maps" "github.com/knadh/listmonk/internal/manager" "github.com/knadh/listmonk/internal/media" @@ -214,28 +214,27 @@ func initImporter(q *Queries, db *sqlx.DB, app *App) *subimporter.Importer { }) } -// initMessengers initializes various messaging backends. +// initMessengers initializes various messenger backends. func initMessengers(m *manager.Manager) messenger.Messenger { - // Load SMTP configurations for the default e-mail Messenger. var ( mapKeys = ko.MapKeys("smtp") srv = make([]messenger.Server, 0, len(mapKeys)) ) + // Load the default SMTP messengers. for _, name := range mapKeys { if !ko.Bool(fmt.Sprintf("smtp.%s.enabled", name)) { lo.Printf("skipped SMTP: %s", name) continue } - var s messenger.Server - if err := ko.Unmarshal("smtp."+name, &s); err != nil { + // Read the SMTP config. + s := messenger.Server{Name: name} + if err := ko.UnmarshalWithConf("smtp."+name, &s, koanf.UnmarshalConf{Tag: "json"}); err != nil { lo.Fatalf("error loading SMTP: %v", err) } - s.Name = name - s.SendTimeout *= time.Millisecond - srv = append(srv, s) + srv = append(srv, s) lo.Printf("loaded SMTP: %s (%s@%s)", s.Name, s.Username, s.Host) } diff --git a/internal/messenger/emailer.go b/internal/messenger/emailer.go index db01d16..2e9f718 100644 --- a/internal/messenger/emailer.go +++ b/internal/messenger/emailer.go @@ -5,10 +5,9 @@ import ( "fmt" "math/rand" "net/smtp" - "time" "github.com/jaytaylor/html2text" - "github.com/jordan-wright/email" + "github.com/knadh/smtppool" ) const emName = "email" @@ -21,21 +20,21 @@ type loginAuth struct { // Server represents an SMTP server's credentials. type Server struct { - Name string - Host string `koanf:"host"` - Port int `koanf:"port"` - AuthProtocol string `koanf:"auth_protocol"` - Username string `koanf:"username"` - Password string `koanf:"password"` - EmailFormat string `koanf:"email_format"` - HelloHostname string `koanf:"hello_hostname"` - SendTimeout time.Duration `koanf:"send_timeout"` - MaxConns int `koanf:"max_conns"` + Name string + Username string `json:"username"` + Password string `json:"password"` + AuthProtocol string `json:"auth_protocol"` + EmailFormat string `json:"email_format"` - mailer *email.Pool + // 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 } -type emailer struct { +// Emailer is the SMTP e-mail messenger. +type Emailer struct { servers map[string]*Server serverNames []string numServers int @@ -43,8 +42,8 @@ type emailer struct { // NewEmailer creates and returns an e-mail Messenger backend. // It takes multiple SMTP configurations. -func NewEmailer(srv ...Server) (Messenger, error) { - e := &emailer{ +func NewEmailer(srv ...Server) (*Emailer, error) { + e := &Emailer{ servers: make(map[string]*Server), } @@ -62,18 +61,14 @@ func NewEmailer(srv ...Server) (Messenger, error) { default: return nil, fmt.Errorf("unknown SMTP auth type '%s'", s.AuthProtocol) } + s.Opt.Auth = auth - pool, err := email.NewPool(fmt.Sprintf("%s:%d", s.Host, s.Port), s.MaxConns, auth) + pool, err := smtppool.New(s.Opt) if err != nil { return nil, err } - // Optional SMTP HELLO hostname. - if server.HelloHostname != "" { - pool.SetHelloHostname(server.HelloHostname) - } - - s.mailer = pool + s.pool = pool e.servers[s.Name] = &s e.serverNames = append(e.serverNames, s.Name) } @@ -83,12 +78,12 @@ func NewEmailer(srv ...Server) (Messenger, error) { } // Name returns the Server's name. -func (e *emailer) Name() string { +func (e *Emailer) Name() string { return emName } // Push pushes a message to the server. -func (e *emailer) Push(fromAddr string, toAddr []string, subject string, m []byte, atts []*Attachment) error { +func (e *Emailer) Push(fromAddr string, toAddr []string, subject string, m []byte, atts []Attachment) error { var key string // If there are more than one SMTP servers, send to a random @@ -100,11 +95,11 @@ func (e *emailer) Push(fromAddr string, toAddr []string, subject string, m []byt } // Are there attachments? - var files []*email.Attachment + var files []smtppool.Attachment if atts != nil { - files = make([]*email.Attachment, 0, len(atts)) + files = make([]smtppool.Attachment, 0, len(atts)) for _, f := range atts { - a := &email.Attachment{ + a := smtppool.Attachment{ Filename: f.Name, Header: f.Header, Content: make([]byte, len(f.Content)), @@ -120,7 +115,7 @@ func (e *emailer) Push(fromAddr string, toAddr []string, subject string, m []byt } srv := e.servers[key] - em := &email.Email{ + em := smtppool.Email{ From: fromAddr, To: toAddr, Subject: subject, @@ -137,11 +132,11 @@ func (e *emailer) Push(fromAddr string, toAddr []string, subject string, m []byt em.Text = []byte(mtext) } - return srv.mailer.Send(em, srv.SendTimeout) + return srv.pool.Send(em) } // Flush flushes the message queue to the server. -func (e *emailer) Flush() error { +func (e *Emailer) Flush() error { return nil } diff --git a/internal/messenger/messenger.go b/internal/messenger/messenger.go index 4f035e6..2b0441c 100644 --- a/internal/messenger/messenger.go +++ b/internal/messenger/messenger.go @@ -6,8 +6,7 @@ import "net/textproto" // for instance, e-mail, SMS etc. type Messenger interface { Name() string - - Push(fromAddr string, toAddr []string, subject string, message []byte, atts []*Attachment) error + Push(fromAddr string, toAddr []string, subject string, message []byte, atts []Attachment) error Flush() error } diff --git a/public.go b/public.go index f4743d9..27ba5be 100644 --- a/public.go +++ b/public.go @@ -360,8 +360,8 @@ func handleSelfExportSubscriberData(c echo.Context) error { []string{data.Email}, "Your profile data", msg.Bytes(), - []*messenger.Attachment{ - &messenger.Attachment{ + []messenger.Attachment{ + { Name: fname, Content: b, Header: messenger.MakeAttachmentHeader(fname, "base64"),