diff --git a/src/notifications/go.mod b/src/notifications/go.mod index 365f0c1e..6b1b170a 100644 --- a/src/notifications/go.mod +++ b/src/notifications/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.1 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/PuerkitoBio/goquery v1.10.0 - github.com/chrj/smtpd v0.0.0-20140720195347-c6fe39d4dcdd + github.com/chrj/smtpd v0.3.1 github.com/go-sql-driver/mysql v1.8.1 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/gorilla/mux v1.8.1 @@ -26,7 +26,6 @@ require ( ) require ( - bitbucket.org/chrj/smtpd v0.0.0-20170817182725-9ddcdbda0f7a // indirect code.cloudfoundry.org/lager v2.0.0+incompatible // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect diff --git a/src/notifications/go.sum b/src/notifications/go.sum index e5259d36..2b0249aa 100644 --- a/src/notifications/go.sum +++ b/src/notifications/go.sum @@ -1,5 +1,3 @@ -bitbucket.org/chrj/smtpd v0.0.0-20170817182725-9ddcdbda0f7a h1:qPZv7NxewNKcHj/TvDO1RbRVGgbsIdzkj4O+9q/5cz4= -bitbucket.org/chrj/smtpd v0.0.0-20170817182725-9ddcdbda0f7a/go.mod h1:rmAH0EKvCdvvOZLc6nphIlzAxGW3Y0Dz0PLoXCJ2YO8= code.cloudfoundry.org/lager v2.0.0+incompatible h1:WZwDKDB2PLd/oL+USK4b4aEjUymIej9My2nUQ9oWEwQ= code.cloudfoundry.org/lager v2.0.0+incompatible/go.mod h1:O2sS7gKP3HM2iemG+EnwvyNQK7pTSC6Foi4QiMp9sSk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -10,8 +8,8 @@ github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbav github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/chrj/smtpd v0.0.0-20140720195347-c6fe39d4dcdd h1:/f/SW/WhVIXuS3c2Eq9w2g0sfvzIAJ+g23YPuNSkKtg= -github.com/chrj/smtpd v0.0.0-20140720195347-c6fe39d4dcdd/go.mod h1:CCN2w0A/V4Mt1XKsMBYtLCUSkPCfanfGjsZbnaJ+13g= +github.com/chrj/smtpd v0.3.1 h1:kogHFkbFdKaoH3bgZkqNC9uVtKYOFfM3uV3rroBdooE= +github.com/chrj/smtpd v0.3.1/go.mod h1:JtABvV/LzvLmEIzy0NyDnrfMGOMd8wy5frAokwf6J9Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/src/notifications/vendor/github.com/chrj/smtpd/.hgignore b/src/notifications/vendor/github.com/chrj/smtpd/.hgignore deleted file mode 100644 index 758d1906..00000000 --- a/src/notifications/vendor/github.com/chrj/smtpd/.hgignore +++ /dev/null @@ -1,3 +0,0 @@ -syntax: glob - -*.orig diff --git a/src/notifications/vendor/github.com/chrj/smtpd/README.md b/src/notifications/vendor/github.com/chrj/smtpd/README.md index 75033b38..e107c3f6 100644 --- a/src/notifications/vendor/github.com/chrj/smtpd/README.md +++ b/src/notifications/vendor/github.com/chrj/smtpd/README.md @@ -1 +1,24 @@ -[![GoDoc](https://godoc.org/bitbucket.org/chrj/smtpd?status.png)](https://godoc.org/bitbucket.org/chrj/smtpd) +Go smtpd [![GoDoc](https://godoc.org/github.com/chrj/smtpd?status.png)](https://godoc.org/github.com/chrj/smtpd) [![Go Report Card](https://goreportcard.com/badge/github.com/chrj/smtpd)](https://goreportcard.com/report/github.com/chrj/smtpd) +======== + +Package smtpd implements an SMTP server in golang. + +Features +-------- + +* STARTTLS (using `crypto/tls`) +* Authentication (PLAIN/LOGIN, only after STARTTLS) +* [XCLIENT](http://www.postfix.org/XCLIENT_README.html) and [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) (for running behind a proxy) +* Connection, HELO, sender and recipient checks for rejecting e-mails using callbacks +* Configurable limits for: connection count, message size and recipient count +* Hands incoming e-mail off to a configured callback function + +Version numbers +--------------- + +The package is tagged with semantic version numbers, making it suitable for use in a [Go Module](https://github.com/golang/go/wiki/Modules). + +Feedback +-------- + +If you end up using this package or have any feedback, I'd very much like to hear about it. You can reach me by [email](mailto:christian@technobabble.dk). diff --git a/src/notifications/vendor/github.com/chrj/smtpd/address.go b/src/notifications/vendor/github.com/chrj/smtpd/address.go index 84bf7353..5cf93d83 100644 --- a/src/notifications/vendor/github.com/chrj/smtpd/address.go +++ b/src/notifications/vendor/github.com/chrj/smtpd/address.go @@ -2,18 +2,21 @@ package smtpd import ( "fmt" - "strings" + "net/mail" ) func parseAddress(src string) (string, error) { - - if src[0] != '<' || src[len(src)-1] != '>' { - return "", fmt.Errorf("Ill-formatted e-mail address: %s", src) - } - - if strings.Count(src, "@") > 1 { - return "", fmt.Errorf("Ill-formatted e-mail address: %s", src) + // While a RFC5321 mailbox specification is not the same as an RFC5322 + // email address specification, it is better to accept that format and + // parse it down to the actual address, as there are a lot of badly + // behaving MTAs and MUAs that do it wrongly. It therefore makes sense + // to rely on Go's built-in address parser. This does have the benefit + // of allowing "email@example.com" as input as thats commonly used, + // though not RFC compliant. + addr, err := mail.ParseAddress(src) + if err != nil { + return "", fmt.Errorf("malformed e-mail address: %s", src) } - return src[1 : len(src)-1], nil + return addr.Address, nil } diff --git a/src/notifications/vendor/github.com/chrj/smtpd/envelope.go b/src/notifications/vendor/github.com/chrj/smtpd/envelope.go new file mode 100644 index 00000000..0fa4fe14 --- /dev/null +++ b/src/notifications/vendor/github.com/chrj/smtpd/envelope.go @@ -0,0 +1,68 @@ +package smtpd + +import ( + "crypto/tls" + "fmt" + "net" + "time" +) + +// Envelope holds a message +type Envelope struct { + Sender string + Recipients []string + Data []byte +} + +// AddReceivedLine prepends a Received header to the Data +func (env *Envelope) AddReceivedLine(peer Peer) { + + tlsDetails := "" + + tlsVersions := map[uint16]string{ + tls.VersionSSL30: "SSL3.0", + tls.VersionTLS10: "TLS1.0", + tls.VersionTLS11: "TLS1.1", + tls.VersionTLS12: "TLS1.2", + tls.VersionTLS13: "TLS1.3", + } + + if peer.TLS != nil { + version := "unknown" + + if val, ok := tlsVersions[peer.TLS.Version]; ok { + version = val + } + + cipher := tls.CipherSuiteName(peer.TLS.CipherSuite) + + tlsDetails = fmt.Sprintf( + "\r\n\t(version=%s cipher=%s);", + version, + cipher, + ) + } + + peerIP := "" + if addr, ok := peer.Addr.(*net.TCPAddr); ok { + peerIP = addr.IP.String() + } + + line := wrap([]byte(fmt.Sprintf( + "Received: from %s ([%s]) by %s with %s;%s\r\n\t%s\r\n", + peer.HeloName, + peerIP, + peer.ServerName, + peer.Protocol, + tlsDetails, + time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700 (MST)"), + ))) + + env.Data = append(env.Data, line...) + + // Move the new Received line up front + + copy(env.Data[len(line):], env.Data[0:len(env.Data)-len(line)]) + copy(env.Data, line) + +} diff --git a/src/notifications/vendor/github.com/chrj/smtpd/protocol.go b/src/notifications/vendor/github.com/chrj/smtpd/protocol.go index 6caaf36b..a047f020 100644 --- a/src/notifications/vendor/github.com/chrj/smtpd/protocol.go +++ b/src/notifications/vendor/github.com/chrj/smtpd/protocol.go @@ -26,10 +26,34 @@ func parseLine(line string) (cmd command) { cmd.line = line cmd.fields = strings.Fields(line) - cmd.action = strings.ToUpper(cmd.fields[0]) - if len(cmd.fields) > 1 { - cmd.params = strings.Split(cmd.fields[1], ":") + if len(cmd.fields) > 0 { + + cmd.action = strings.ToUpper(cmd.fields[0]) + + if len(cmd.fields) > 1 { + + // Account for some clients breaking the standard and having + // an extra whitespace after the ':' character. Example: + // + // MAIL FROM: + // + // Should be: + // + // MAIL FROM: + // + // Thus, we add a check if the second field ends with ':' + // and appends the rest of the third field. + + if cmd.fields[1][len(cmd.fields[1])-1] == ':' && len(cmd.fields) > 2 { + cmd.fields[1] = cmd.fields[1] + cmd.fields[2] + cmd.fields = cmd.fields[0:2] + } + + cmd.params = strings.Split(cmd.fields[1], ":") + + } + } return @@ -46,6 +70,10 @@ func (session *session) handle(line string) { switch cmd.action { + case "PROXY": + session.handlePROXY(cmd) + return + case "HELO": session.handleHELO(cmd) return @@ -147,6 +175,8 @@ func (session *session) handleEHLO(cmd command) { session.peer.HeloName = cmd.fields[1] session.peer.Protocol = ESMTP + fmt.Fprintf(session.writer, "250-%s\r\n", session.server.Hostname) + extensions := session.extensions() if len(extensions) > 1 { @@ -162,12 +192,21 @@ func (session *session) handleEHLO(cmd command) { } func (session *session) handleMAIL(cmd command) { + if len(cmd.params) != 2 || strings.ToUpper(cmd.params[0]) != "FROM" { + session.reply(502, "Invalid syntax.") + return + } if session.peer.HeloName == "" { session.reply(502, "Please introduce yourself first.") return } + if session.server.Authenticator != nil && session.peer.Username == "" { + session.reply(530, "Authentication Required.") + return + } + if !session.tls && session.server.ForceTLS { session.reply(502, "Please turn on TLS by issuing a STARTTLS command.") return @@ -178,11 +217,17 @@ func (session *session) handleMAIL(cmd command) { return } - addr, err := parseAddress(cmd.params[1]) + var err error + addr := "" // null sender - if err != nil { - session.reply(502, "Ill-formatted e-mail address") - return + // We must accept a null sender as per rfc5321 section-6.1. + if cmd.params[1] != "<>" { + addr, err = parseAddress(cmd.params[1]) + + if err != nil { + session.reply(502, "Malformed e-mail address") + return + } } if session.server.SenderChecker != nil { @@ -204,6 +249,10 @@ func (session *session) handleMAIL(cmd command) { } func (session *session) handleRCPT(cmd command) { + if len(cmd.params) != 2 || strings.ToUpper(cmd.params[0]) != "TO" { + session.reply(502, "Invalid syntax.") + return + } if session.envelope == nil { session.reply(502, "Missing MAIL FROM command.") @@ -218,7 +267,7 @@ func (session *session) handleRCPT(cmd command) { addr, err := parseAddress(cmd.params[1]) if err != nil { - session.reply(502, "Ill-formatted e-mail address") + session.reply(502, "Malformed e-mail address") return } @@ -254,6 +303,7 @@ func (session *session) handleSTARTTLS(cmd command) { session.reply(220, "Go ahead") if err := tlsConn.Handshake(); err != nil { + session.logError(err, "couldn't perform handshake") session.reply(550, "Handshake error") return } @@ -272,6 +322,10 @@ func (session *session) handleSTARTTLS(cmd command) { session.scanner = bufio.NewScanner(session.reader) session.tls = true + // Save connection state on peer + state := tlsConn.ConnectionState() + session.peer.TLS = &state + // Flush the connection to set new timeout deadlines session.flush() @@ -353,6 +407,10 @@ func (session *session) handleQUIT(cmd command) { } func (session *session) handleAUTH(cmd command) { + if len(cmd.fields) < 2 { + session.reply(502, "Invalid syntax.") + return + } if session.server.Authenticator == nil { session.reply(502, "AUTH not supported.") @@ -409,13 +467,19 @@ func (session *session) handleAUTH(cmd command) { case "LOGIN": - session.reply(334, "VXNlcm5hbWU6") + encodedUsername := "" - if !session.scanner.Scan() { - return + if len(cmd.fields) < 3 { + session.reply(334, "VXNlcm5hbWU6") + if !session.scanner.Scan() { + return + } + encodedUsername = session.scanner.Text() + } else { + encodedUsername = cmd.fields[2] } - byteUsername, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + byteUsername, err := base64.StdEncoding.DecodeString(encodedUsername) if err != nil { session.reply(502, "Couldn't decode your credentials") @@ -440,6 +504,7 @@ func (session *session) handleAUTH(cmd command) { default: + session.logf("unknown authentication mechanism: %s", mechanism) session.reply(502, "Unknown authentication mechanism") return @@ -459,6 +524,10 @@ func (session *session) handleAUTH(cmd command) { } func (session *session) handleXCLIENT(cmd command) { + if len(cmd.fields) < 2 { + session.reply(502, "Invalid syntax.") + return + } if !session.server.EnableXCLIENT { session.reply(550, "XCLIENT not enabled") @@ -466,11 +535,10 @@ func (session *session) handleXCLIENT(cmd command) { } var ( - newHeloName = "" - newAddr net.IP = nil - newTCPPort uint64 = 0 - newUsername = "" - newProto Protocol = "" + newHeloName, newUsername string + newProto Protocol + newAddr net.IP + newTCPPort uint64 ) for _, item := range cmd.fields[1:] { @@ -556,3 +624,47 @@ func (session *session) handleXCLIENT(cmd command) { session.welcome() } + +func (session *session) handlePROXY(cmd command) { + + if !session.server.EnableProxyProtocol { + session.reply(550, "Proxy Protocol not enabled") + return + } + + if len(cmd.fields) < 6 { + session.reply(502, "Couldn't decode the command.") + return + } + + var ( + newAddr net.IP = nil + newTCPPort uint64 = 0 + err error + ) + + newAddr = net.ParseIP(cmd.fields[2]) + + newTCPPort, err = strconv.ParseUint(cmd.fields[4], 10, 16) + if err != nil { + session.reply(502, "Couldn't decode the command.") + return + } + + tcpAddr, ok := session.peer.Addr.(*net.TCPAddr) + if !ok { + session.reply(502, "Unsupported network connection") + return + } + + if newAddr != nil { + tcpAddr.IP = newAddr + } + + if newTCPPort != 0 { + tcpAddr.Port = int(newTCPPort) + } + + session.welcome() + +} diff --git a/src/notifications/vendor/github.com/chrj/smtpd/smtpd.go b/src/notifications/vendor/github.com/chrj/smtpd/smtpd.go index b6a07c7e..852ff307 100644 --- a/src/notifications/vendor/github.com/chrj/smtpd/smtpd.go +++ b/src/notifications/vendor/github.com/chrj/smtpd/smtpd.go @@ -4,16 +4,19 @@ package smtpd import ( "bufio" "crypto/tls" + "errors" "fmt" "log" "net" - "os" + "strings" + "sync" + "sync/atomic" "time" ) // Server defines the parameters for running the SMTP server type Server struct { - Addr string // Address to listen on when using ListenAndServe. (default: "127.0.0.1:10025") + Hostname string // Server hostname. (default: "localhost.localdomain") WelcomeMessage string // Initial server banner. (default: " ESMTP ready.") ReadTimeout time.Duration // Socket timeout for read operations. (default: 60s) @@ -42,34 +45,43 @@ type Server struct { // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error - EnableXCLIENT bool // Enable XCLIENT support (default: false) + EnableXCLIENT bool // Enable XCLIENT support (default: false) + EnableProxyProtocol bool // Enable proxy protocol support (default: false) TLSConfig *tls.Config // Enable STARTTLS support. ForceTLS bool // Force STARTTLS usage. + + ProtocolLogger *log.Logger + + // mu guards doneChan and makes closing it and listener atomic from + // perspective of Serve() + mu sync.Mutex + doneChan chan struct{} + listener *net.Listener + waitgrp sync.WaitGroup + inShutdown atomicBool // true when server is in shutdown } // Protocol represents the protocol used in the SMTP session type Protocol string const ( - SMTP Protocol = "SMTP" - ESMTP = "ESMTP" + // SMTP + SMTP Protocol = "SMTP" + + // Extended SMTP + ESMTP = "ESMTP" ) // Peer represents the client connecting to the server type Peer struct { - HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication, if authenticated - Password string // Password from authentication, if authenticated - Protocol Protocol // Protocol used, SMTP or ESMTP - Addr net.Addr // Network address -} - -// Envelope holds a message -type Envelope struct { - Sender string - Recipients []string - Data []byte + HeloName string // Server name used in HELO/EHLO command + Username string // Username from authentication, if authenticated + Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP + ServerName string // A copy of Server.Hostname + Addr net.Addr // Network address + TLS *tls.ConnectionState // TLS Connection details, if on TLS } // Error represents an Error reported in the SMTP session. @@ -81,6 +93,10 @@ type Error struct { // Error returns a string representation of the SMTP error func (e Error) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Message) } +// ErrServerClosed is returned by the Server's Serve and ListenAndServe, +// methods after a call to Shutdown. +var ErrServerClosed = errors.New("smtp: Server closed") + type session struct { server *Server @@ -103,7 +119,26 @@ func (srv *Server) newSession(c net.Conn) (s *session) { conn: c, reader: bufio.NewReader(c), writer: bufio.NewWriter(c), - peer: Peer{Addr: c.RemoteAddr()}, + peer: Peer{ + Addr: c.RemoteAddr(), + ServerName: srv.Hostname, + }, + } + + // Check if the underlying connection is already TLS. + // This will happen if the Listerner provided Serve() + // is from tls.Listen() + + var tlsConn *tls.Conn + + tlsConn, s.tls = c.(*tls.Conn) + + if s.tls { + // run handshake otherwise it's done when we first + // read/write and connection state will be invalid + tlsConn.Handshake() + state := tlsConn.ConnectionState() + s.peer.TLS = &state } s.scanner = bufio.NewScanner(s.reader) @@ -112,12 +147,15 @@ func (srv *Server) newSession(c net.Conn) (s *session) { } -// ListenAndServe starts the SMTP server and listens on the address provided in Server.Addr -func (srv *Server) ListenAndServe() error { +// ListenAndServe starts the SMTP server and listens on the address provided +func (srv *Server) ListenAndServe(addr string) error { + if srv.shuttingDown() { + return ErrServerClosed + } srv.configureDefaults() - l, err := net.Listen("tcp", srv.Addr) + l, err := net.Listen("tcp", addr) if err != nil { return err } @@ -127,23 +165,31 @@ func (srv *Server) ListenAndServe() error { // Serve starts the SMTP server and listens on the Listener provided func (srv *Server) Serve(l net.Listener) error { + if srv.shuttingDown() { + return ErrServerClosed + } srv.configureDefaults() + l = &onceCloseListener{Listener: l} defer l.Close() + srv.listener = &l var limiter chan struct{} if srv.MaxConnections > 0 { limiter = make(chan struct{}, srv.MaxConnections) - } else { - limiter = nil } for { - conn, e := l.Accept() if e != nil { + select { + case <-srv.getDoneChan(): + return ErrServerClosed + default: + } + if ne, ok := e.(net.Error); ok && ne.Temporary() { time.Sleep(time.Second) continue @@ -153,8 +199,10 @@ func (srv *Server) Serve(l net.Listener) error { session := srv.newSession(conn) - if limiter != nil { - go func() { + srv.waitgrp.Add(1) + go func() { + defer srv.waitgrp.Done() + if limiter != nil { select { case limiter <- struct{}{}: session.serve() @@ -162,13 +210,51 @@ func (srv *Server) Serve(l net.Listener) error { default: session.reject() } - }() - } else { - go session.serve() - } + } else { + session.serve() + } + }() + } + +} + +// Shutdown instructs the server to shutdown, starting by closing the +// associated listener. If wait is true, it will wait for the shutdown +// to complete. If wait is false, Wait must be called afterwards. +func (srv *Server) Shutdown(wait bool) error { + var lnerr error + srv.inShutdown.setTrue() + + // First close the listener + srv.mu.Lock() + if srv.listener != nil { + lnerr = (*srv.listener).Close(); + } + srv.closeDoneChanLocked() + srv.mu.Unlock() + // Now wait for all client connections to close + if wait { + srv.Wait() } + return lnerr +} + +// Wait waits for all client connections to close and the server to finish +// shutting down. +func (srv *Server) Wait() error { + if !srv.shuttingDown() { + return errors.New("Server has not been Shutdown") + } + + srv.waitgrp.Wait() + return nil +} + +// Address returns the listening address of the server +func (srv *Server) Address() net.Addr { + return (*srv.listener).Addr(); } func (srv *Server) configureDefaults() { @@ -201,20 +287,12 @@ func (srv *Server) configureDefaults() { log.Fatal("Cannot use ForceTLS with no TLSConfig") } - if srv.Addr == "" { - srv.Addr = "127.0.0.1:10025" + if srv.Hostname == "" { + srv.Hostname = "localhost.localdomain" } if srv.WelcomeMessage == "" { - - hostname, err := os.Hostname() - - if err != nil { - log.Fatal("Couldn't determine hostname: %s", err) - } - - srv.WelcomeMessage = fmt.Sprintf("%s ESMTP ready.", hostname) - + srv.WelcomeMessage = fmt.Sprintf("%s ESMTP ready.", srv.Hostname) } } @@ -223,12 +301,16 @@ func (session *session) serve() { defer session.close() - session.welcome() + if !session.server.EnableProxyProtocol { + session.welcome() + } for { for session.scanner.Scan() { - session.handle(session.scanner.Text()) + line := session.scanner.Text() + session.logf("received: %s", strings.TrimSpace(line)) + session.handle(line) } err := session.scanner.Err() @@ -279,6 +361,7 @@ func (session *session) welcome() { } func (session *session) reply(code int, message string) { + session.logf("sending: %d %s", code, message) fmt.Fprintf(session.writer, "%d %s\r\n", code, message) session.flush() } @@ -297,6 +380,22 @@ func (session *session) error(err error) { } } +func (session *session) logf(format string, v ...interface{}) { + if session.server.ProtocolLogger == nil { + return + } + session.server.ProtocolLogger.Output(2, fmt.Sprintf( + "%s [peer:%s]", + fmt.Sprintf(format, v...), + session.peer.Addr, + )) + +} + +func (session *session) logError(err error, desc string) { + session.logf("%s: %v ", desc, err) +} + func (session *session) extensions() []string { extensions := []string{ @@ -333,3 +432,56 @@ func (session *session) close() { time.Sleep(200 * time.Millisecond) session.conn.Close() } + + +// From net/http/server.go + +func (s *Server) shuttingDown() bool { + return s.inShutdown.isSet() +} + +func (s *Server) getDoneChan() <-chan struct{} { + s.mu.Lock() + defer s.mu.Unlock() + return s.getDoneChanLocked() +} + +func (s *Server) getDoneChanLocked() chan struct{} { + if s.doneChan == nil { + s.doneChan = make(chan struct{}) + } + return s.doneChan +} + +func (s *Server) closeDoneChanLocked() { + ch := s.getDoneChanLocked() + select { + case <-ch: + // Already closed. Don't close again. + default: + // Safe to close here. We're the only closer, guarded + // by s.mu. + close(ch) + } +} + +// onceCloseListener wraps a net.Listener, protecting it from +// multiple Close calls. +type onceCloseListener struct { + net.Listener + once sync.Once + closeErr error +} + +func (oc *onceCloseListener) Close() error { + oc.once.Do(oc.close) + return oc.closeErr +} + +func (oc *onceCloseListener) close() { oc.closeErr = oc.Listener.Close() } + +type atomicBool int32 + +func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } +func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } +func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) } diff --git a/src/notifications/vendor/github.com/chrj/smtpd/wrap.go b/src/notifications/vendor/github.com/chrj/smtpd/wrap.go new file mode 100644 index 00000000..91a6cce2 --- /dev/null +++ b/src/notifications/vendor/github.com/chrj/smtpd/wrap.go @@ -0,0 +1,22 @@ +package smtpd + +// Wrap a byte slice paragraph for use in SMTP header +func wrap(sl []byte) []byte { + length := 0 + for i := 0; i < len(sl); i++ { + if length > 76 && sl[i] == ' ' { + sl = append(sl, 0, 0) + copy(sl[i+2:], sl[i:]) + sl[i] = '\r' + sl[i+1] = '\n' + sl[i+2] = '\t' + i += 2 + length = 0 + } + if sl[i] == '\n' { + length = 0 + } + length++ + } + return sl +} diff --git a/src/notifications/vendor/modules.txt b/src/notifications/vendor/modules.txt index 63c5d075..4aa67273 100644 --- a/src/notifications/vendor/modules.txt +++ b/src/notifications/vendor/modules.txt @@ -1,5 +1,3 @@ -# bitbucket.org/chrj/smtpd v0.0.0-20170817182725-9ddcdbda0f7a -## explicit # code.cloudfoundry.org/lager v2.0.0+incompatible ## explicit # filippo.io/edwards25519 v1.1.0 @@ -15,8 +13,8 @@ github.com/PuerkitoBio/goquery # github.com/andybalholm/cascadia v1.3.2 ## explicit; go 1.16 github.com/andybalholm/cascadia -# github.com/chrj/smtpd v0.0.0-20140720195347-c6fe39d4dcdd -## explicit +# github.com/chrj/smtpd v0.3.1 +## explicit; go 1.14 github.com/chrj/smtpd # github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible ## explicit