upgrade deps; rewrite smtp session
This commit is contained in:
19
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
19
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
@@ -2,12 +2,9 @@ package dkim
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var rxReduceWS = regexp.MustCompile(`[ \t\r\n]+`)
|
||||
|
||||
// Canonicalization is a canonicalization algorithm.
|
||||
type Canonicalization string
|
||||
|
||||
@@ -113,17 +110,15 @@ func (c *simpleCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
|
||||
type relaxedCanonicalizer struct{}
|
||||
|
||||
func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string {
|
||||
kv := strings.SplitN(s, ":", 2)
|
||||
|
||||
k := strings.TrimSpace(strings.ToLower(kv[0]))
|
||||
|
||||
var v string
|
||||
if len(kv) > 1 {
|
||||
v = rxReduceWS.ReplaceAllString(kv[1], " ")
|
||||
v = strings.TrimSpace(v)
|
||||
|
||||
k, v, ok := strings.Cut(s, ":")
|
||||
if !ok {
|
||||
return strings.TrimSpace(strings.ToLower(s)) + ":" + crlf
|
||||
}
|
||||
|
||||
k = strings.TrimSpace(strings.ToLower(k))
|
||||
v = strings.Join(strings.FieldsFunc(v, func(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
}), " ")
|
||||
return k + ":" + v + crlf
|
||||
}
|
||||
|
||||
|
||||
13
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
13
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
@@ -1,4 +1,17 @@
|
||||
// Package dkim creates and verifies DKIM signatures, as specified in RFC 6376.
|
||||
//
|
||||
// # FAQ
|
||||
//
|
||||
// Why can't I verify a [net/mail.Message] directly? A [net/mail.Message]
|
||||
// header is already parsed, and whitespace characters (especially continuation
|
||||
// lines) are removed. Thus, the signature computed from the parsed header is
|
||||
// not the same as the one computed from the raw header.
|
||||
//
|
||||
// How can I publish my public key? You have to add a TXT record to your DNS
|
||||
// zone. See [RFC 6376 appendix C]. You can use the dkim-keygen tool included
|
||||
// in go-msgauth to generate the key and the TXT record.
|
||||
//
|
||||
// [RFC 6376 appendix C]: https://tools.ietf.org/html/rfc6376#appendix-C
|
||||
package dkim
|
||||
|
||||
import (
|
||||
|
||||
18
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
18
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
@@ -66,28 +66,24 @@ func foldHeaderField(kv string) string {
|
||||
return fold.String() + crlf
|
||||
}
|
||||
|
||||
func parseHeaderField(s string) (k string, v string) {
|
||||
kv := strings.SplitN(s, ":", 2)
|
||||
k = strings.TrimSpace(kv[0])
|
||||
if len(kv) > 1 {
|
||||
v = strings.TrimSpace(kv[1])
|
||||
}
|
||||
return
|
||||
func parseHeaderField(s string) (string, string) {
|
||||
key, value, _ := strings.Cut(s, ":")
|
||||
return strings.TrimSpace(key), strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func parseHeaderParams(s string) (map[string]string, error) {
|
||||
pairs := strings.Split(s, ";")
|
||||
params := make(map[string]string)
|
||||
for _, s := range pairs {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
key, value, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
return params, errors.New("dkim: malformed header params")
|
||||
}
|
||||
|
||||
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||||
params[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
@@ -149,6 +145,8 @@ func newHeaderPicker(h header) *headerPicker {
|
||||
}
|
||||
|
||||
func (p *headerPicker) Pick(key string) string {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
at := p.picked[key]
|
||||
for i := len(p.h) - 1; i >= 0; i-- {
|
||||
kv := p.h[i]
|
||||
|
||||
27
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
27
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
@@ -70,24 +70,31 @@ var queryMethods = map[QueryMethod]queryFunc{
|
||||
}
|
||||
|
||||
func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
|
||||
var txts []string
|
||||
var err error
|
||||
if txtLookup != nil {
|
||||
txts, err = txtLookup(selector + "._domainkey." + domain)
|
||||
} else {
|
||||
txts, err = net.LookupTXT(selector + "._domainkey." + domain)
|
||||
if txtLookup == nil {
|
||||
txtLookup = net.LookupTXT
|
||||
}
|
||||
|
||||
txts, err := txtLookup(selector + "._domainkey." + domain)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||
return nil, tempFailError("key unavailable: " + err.Error())
|
||||
} else if err != nil {
|
||||
return nil, permFailError("no key for signature: " + err.Error())
|
||||
}
|
||||
|
||||
// Long keys are split in multiple parts
|
||||
txt := strings.Join(txts, "")
|
||||
|
||||
return parsePublicKey(txt)
|
||||
// net.LookupTXT will concatenate strings contained in a single TXT record.
|
||||
// In other words, net.LookupTXT returns one entry per TXT record, even if
|
||||
// a record contains multiple strings.
|
||||
//
|
||||
// RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
|
||||
// behavior, so reject that.
|
||||
switch len(txts) {
|
||||
case 0:
|
||||
return nil, permFailError("no valid key found")
|
||||
case 1:
|
||||
return parsePublicKey(txts[0])
|
||||
default:
|
||||
return nil, permFailError("multiple TXT records found for key")
|
||||
}
|
||||
}
|
||||
|
||||
func parsePublicKey(s string) (*queryResult, error) {
|
||||
|
||||
2
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
2
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
@@ -74,7 +74,7 @@ type SignOptions struct {
|
||||
//
|
||||
// The whole message header and body must be written to the Signer. Close should
|
||||
// always be called (either after the whole message has been written, or after
|
||||
// an error occured and the signer won't be used anymore). Close may return an
|
||||
// an error occurred and the signer won't be used anymore). Close may return an
|
||||
// error in case signing fails.
|
||||
//
|
||||
// After a successful Close, Signature can be called to retrieve the
|
||||
|
||||
10
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
10
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
@@ -293,12 +293,10 @@ func verify(h header, r io.Reader, sigField, sigValue string, options *VerifyOpt
|
||||
}
|
||||
|
||||
// Parse algos
|
||||
algos := strings.SplitN(stripWhitespace(params["a"]), "-", 2)
|
||||
if len(algos) != 2 {
|
||||
keyAlgo, hashAlgo, ok := strings.Cut(stripWhitespace(params["a"]), "-")
|
||||
if !ok {
|
||||
return verif, permFailError("malformed algorithm name")
|
||||
}
|
||||
keyAlgo := algos[0]
|
||||
hashAlgo := algos[1]
|
||||
|
||||
// Check hash algo
|
||||
if res.HashAlgos != nil {
|
||||
@@ -457,6 +455,8 @@ func stripWhitespace(s string) string {
|
||||
}, s)
|
||||
}
|
||||
|
||||
var sigRegex = regexp.MustCompile(`(b\s*=)[^;]+`)
|
||||
|
||||
func removeSignature(s string) string {
|
||||
return regexp.MustCompile(`(b\s*=)[^;]+`).ReplaceAllString(s, "$1")
|
||||
return sigRegex.ReplaceAllString(s, "$1")
|
||||
}
|
||||
|
||||
3
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
3
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
@@ -1,11 +1,12 @@
|
||||
# go-sasl
|
||||
|
||||
[](https://godoc.org/github.com/emersion/go-sasl)
|
||||
[](https://godocs.io/github.com/emersion/go-sasl)
|
||||
[](https://travis-ci.org/emersion/go-sasl)
|
||||
|
||||
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
|
||||
|
||||
Implemented mechanisms:
|
||||
|
||||
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
|
||||
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
|
||||
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)
|
||||
|
||||
2
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
2
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
@@ -27,7 +27,7 @@ func NewAnonymousClient(trace string) Client {
|
||||
type AnonymousAuthenticator func(trace string) error
|
||||
|
||||
type anonymousServer struct {
|
||||
done bool
|
||||
done bool
|
||||
authenticate AnonymousAuthenticator
|
||||
}
|
||||
|
||||
|
||||
41
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
41
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
@@ -1,5 +1,10 @@
|
||||
package sasl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// The EXTERNAL mechanism name.
|
||||
const External = "EXTERNAL"
|
||||
|
||||
@@ -24,3 +29,39 @@ func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
|
||||
func NewExternalClient(identity string) Client {
|
||||
return &externalClient{identity}
|
||||
}
|
||||
|
||||
// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If
|
||||
// the identity is left blank, it indicates that it is the same as the one used
|
||||
// in the external credentials. If identity is not empty and the server doesn't
|
||||
// support it, an error must be returned.
|
||||
type ExternalAuthenticator func(identity string) error
|
||||
|
||||
type externalServer struct {
|
||||
done bool
|
||||
authenticate ExternalAuthenticator
|
||||
}
|
||||
|
||||
func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
if a.done {
|
||||
return nil, false, ErrUnexpectedClientResponse
|
||||
}
|
||||
|
||||
// No initial response, send an empty challenge
|
||||
if response == nil {
|
||||
return []byte{}, false, nil
|
||||
}
|
||||
|
||||
a.done = true
|
||||
|
||||
if bytes.Contains(response, []byte("\x00")) {
|
||||
return nil, false, errors.New("sasl: identity contains a NUL character")
|
||||
}
|
||||
|
||||
return nil, true, a.authenticate(string(response))
|
||||
}
|
||||
|
||||
// NewExternalServer creates a server implementation of the EXTERNAL
|
||||
// authentication mechanism, as described in RFC 4422.
|
||||
func NewExternalServer(authenticator ExternalAuthenticator) Server {
|
||||
return &externalServer{authenticate: authenticator}
|
||||
}
|
||||
|
||||
27
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
27
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
@@ -35,8 +35,11 @@ type oauthBearerClient struct {
|
||||
}
|
||||
|
||||
func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
|
||||
mech = OAuthBearer
|
||||
var str = "n,a=" + a.Username + ","
|
||||
var authzid string
|
||||
if a.Username != "" {
|
||||
authzid = "a=" + a.Username
|
||||
}
|
||||
str := "n," + authzid + ","
|
||||
|
||||
if a.Host != "" {
|
||||
str += "\x01host=" + a.Host
|
||||
@@ -47,7 +50,7 @@ func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
|
||||
}
|
||||
str += "\x01auth=Bearer " + a.Token + "\x01\x01"
|
||||
ir = []byte(str)
|
||||
return
|
||||
return OAuthBearer, ir, nil
|
||||
}
|
||||
|
||||
func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
|
||||
@@ -81,7 +84,7 @@ func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
|
||||
if err != nil {
|
||||
panic(err) // wtf
|
||||
}
|
||||
a.failErr = errors.New(descr)
|
||||
a.failErr = errors.New("sasl: client error: " + descr)
|
||||
return blob, false, nil
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
|
||||
// indirectly OAUTHBEARER) defines a protocol-independent way to do so
|
||||
// using 0x01.
|
||||
if len(response) != 1 && response[0] != 0x01 {
|
||||
return nil, true, errors.New("unexpected response")
|
||||
return nil, true, errors.New("sasl: invalid response")
|
||||
}
|
||||
return nil, true, a.failErr
|
||||
}
|
||||
@@ -121,14 +124,18 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
|
||||
if len(parts) != 3 {
|
||||
return a.fail("Invalid response")
|
||||
}
|
||||
if !bytes.Equal(parts[0], []byte{'n'}) {
|
||||
return a.fail("Invalid response, missing 'n'")
|
||||
flag := parts[0]
|
||||
authzid := parts[1]
|
||||
if !bytes.Equal(flag, []byte{'n'}) {
|
||||
return a.fail("Invalid response, missing 'n' in gs2-cb-flag")
|
||||
}
|
||||
opts := OAuthBearerOptions{}
|
||||
if !bytes.HasPrefix(parts[1], []byte("a=")) {
|
||||
return a.fail("Invalid response, missing 'a'")
|
||||
if len(authzid) > 0 {
|
||||
if !bytes.HasPrefix(authzid, []byte("a=")) {
|
||||
return a.fail("Invalid response, missing 'a=' in gs2-authzid")
|
||||
}
|
||||
opts.Username = string(bytes.TrimPrefix(authzid, []byte("a=")))
|
||||
}
|
||||
opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a=")))
|
||||
|
||||
// Cut \x01host=...\x01auth=...\x01\x01
|
||||
// into
|
||||
|
||||
4
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
4
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
@@ -38,7 +38,7 @@ func NewPlainClient(identity, username, password string) Client {
|
||||
type PlainAuthenticator func(identity, username, password string) error
|
||||
|
||||
type plainServer struct {
|
||||
done bool
|
||||
done bool
|
||||
authenticate PlainAuthenticator
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err er
|
||||
|
||||
parts := bytes.Split(response, []byte("\x00"))
|
||||
if len(parts) != 3 {
|
||||
err = errors.New("Invalid response")
|
||||
err = errors.New("sasl: invalid response")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
2
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
2
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// Common SASL errors.
|
||||
var (
|
||||
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
|
||||
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
|
||||
ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
|
||||
)
|
||||
|
||||
|
||||
10
vendor/github.com/emersion/go-smtp/.build.yml
generated
vendored
10
vendor/github.com/emersion/go-smtp/.build.yml
generated
vendored
@@ -1,11 +1,10 @@
|
||||
image: alpine/edge
|
||||
packages:
|
||||
- go
|
||||
# Required by codecov
|
||||
- bash
|
||||
- findutils
|
||||
sources:
|
||||
- https://github.com/emersion/go-smtp
|
||||
artifacts:
|
||||
- coverage.html
|
||||
tasks:
|
||||
- build: |
|
||||
cd go-smtp
|
||||
@@ -13,7 +12,6 @@ tasks:
|
||||
- test: |
|
||||
cd go-smtp
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- upload-coverage: |
|
||||
- coverage: |
|
||||
cd go-smtp
|
||||
export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
|
||||
curl -s https://codecov.io/bash | bash
|
||||
go tool cover -html=coverage.txt -o ~/coverage.html
|
||||
|
||||
141
vendor/github.com/emersion/go-smtp/README.md
generated
vendored
141
vendor/github.com/emersion/go-smtp/README.md
generated
vendored
@@ -1,144 +1,16 @@
|
||||
# go-smtp
|
||||
|
||||
[](https://godocs.io/github.com/emersion/go-smtp)
|
||||
[](https://pkg.go.dev/github.com/emersion/go-smtp)
|
||||
[](https://builds.sr.ht/~emersion/go-smtp/commits?)
|
||||
[](https://codecov.io/gh/emersion/go-smtp)
|
||||
|
||||
An ESMTP client and server library written in Go.
|
||||
|
||||
## Features
|
||||
|
||||
* ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321)
|
||||
* Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920)
|
||||
* ESMTP client & server implementing [RFC 5321]
|
||||
* Support for additional SMTP extensions such as [AUTH] and [PIPELINING]
|
||||
* UTF-8 support for subject and message
|
||||
* [LMTP](https://tools.ietf.org/html/rfc2033) support
|
||||
|
||||
## Usage
|
||||
|
||||
### Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set up authentication information.
|
||||
auth := sasl.NewPlainClient("", "user@example.com", "password")
|
||||
|
||||
// Connect to the server, authenticate, set the sender and recipient,
|
||||
// and send the email all in one step.
|
||||
to := []string{"recipient@example.net"}
|
||||
msg := strings.NewReader("To: recipient@example.net\r\n" +
|
||||
"Subject: discount Gophers!\r\n" +
|
||||
"\r\n" +
|
||||
"This is the email body.\r\n")
|
||||
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you need more control, you can use `Client` instead.
|
||||
|
||||
### Server
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-smtp"
|
||||
)
|
||||
|
||||
// The Backend implements SMTP server methods.
|
||||
type Backend struct{}
|
||||
|
||||
// Login handles a login command with username and password.
|
||||
func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
||||
if username != "username" || password != "password" {
|
||||
return nil, errors.New("Invalid username or password")
|
||||
}
|
||||
return &Session{}, nil
|
||||
}
|
||||
|
||||
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
|
||||
func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||
return nil, smtp.ErrAuthRequired
|
||||
}
|
||||
|
||||
// A Session is returned after successful login.
|
||||
type Session struct{}
|
||||
|
||||
func (s *Session) Mail(from string, opts smtp.MailOptions) error {
|
||||
log.Println("Mail from:", from)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Rcpt(to string) error {
|
||||
log.Println("Rcpt to:", to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Data(r io.Reader) error {
|
||||
if b, err := ioutil.ReadAll(r); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Println("Data:", string(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Reset() {}
|
||||
|
||||
func (s *Session) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
be := &Backend{}
|
||||
|
||||
s := smtp.NewServer(be)
|
||||
|
||||
s.Addr = ":1025"
|
||||
s.Domain = "localhost"
|
||||
s.ReadTimeout = 10 * time.Second
|
||||
s.WriteTimeout = 10 * time.Second
|
||||
s.MaxMessageBytes = 1024 * 1024
|
||||
s.MaxRecipients = 50
|
||||
s.AllowInsecureAuth = true
|
||||
|
||||
log.Println("Starting server at", s.Addr)
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the server manually with `telnet`:
|
||||
```
|
||||
$ telnet localhost 1025
|
||||
EHLO localhost
|
||||
AUTH PLAIN
|
||||
AHVzZXJuYW1lAHBhc3N3b3Jk
|
||||
MAIL FROM:<root@nsa.gov>
|
||||
RCPT TO:<root@gchq.gov.uk>
|
||||
DATA
|
||||
Hey <3
|
||||
.
|
||||
```
|
||||
* [LMTP] support
|
||||
|
||||
## Relationship with net/smtp
|
||||
|
||||
@@ -149,3 +21,8 @@ provides a server implementation and a number of client improvements.
|
||||
## Licence
|
||||
|
||||
MIT
|
||||
|
||||
[RFC 5321]: https://tools.ietf.org/html/rfc5321
|
||||
[AUTH]: https://tools.ietf.org/html/rfc4954
|
||||
[PIPELINING]: https://tools.ietf.org/html/rfc2920
|
||||
[LMTP]: https://tools.ietf.org/html/rfc2033
|
||||
|
||||
74
vendor/github.com/emersion/go-smtp/backend.go
generated
vendored
74
vendor/github.com/emersion/go-smtp/backend.go
generated
vendored
@@ -1,61 +1,30 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAuthRequired = errors.New("Please authenticate first")
|
||||
ErrAuthUnsupported = errors.New("Authentication not supported")
|
||||
ErrAuthFailed = &SMTPError{
|
||||
Code: 535,
|
||||
EnhancedCode: EnhancedCode{5, 7, 8},
|
||||
Message: "Authentication failed",
|
||||
}
|
||||
ErrAuthRequired = &SMTPError{
|
||||
Code: 502,
|
||||
EnhancedCode: EnhancedCode{5, 7, 0},
|
||||
Message: "Please authenticate first",
|
||||
}
|
||||
ErrAuthUnsupported = &SMTPError{
|
||||
Code: 502,
|
||||
EnhancedCode: EnhancedCode{5, 7, 0},
|
||||
Message: "Authentication not supported",
|
||||
}
|
||||
)
|
||||
|
||||
// A SMTP server backend.
|
||||
type Backend interface {
|
||||
// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to
|
||||
// support this.
|
||||
Login(state *ConnectionState, username, password string) (Session, error)
|
||||
|
||||
// Called if the client attempts to send mail without logging in first.
|
||||
// Return smtp.ErrAuthRequired if you don't want to support this.
|
||||
AnonymousLogin(state *ConnectionState) (Session, error)
|
||||
}
|
||||
|
||||
type BodyType string
|
||||
|
||||
const (
|
||||
Body7Bit BodyType = "7BIT"
|
||||
Body8BitMIME BodyType = "8BITMIME"
|
||||
BodyBinaryMIME BodyType = "BINARYMIME"
|
||||
)
|
||||
|
||||
// MailOptions contains custom arguments that were
|
||||
// passed as an argument to the MAIL command.
|
||||
type MailOptions struct {
|
||||
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
|
||||
Body BodyType
|
||||
|
||||
// Size of the body. Can be 0 if not specified by client.
|
||||
Size int
|
||||
|
||||
// TLS is required for the message transmission.
|
||||
//
|
||||
// The message should be rejected if it can't be transmitted
|
||||
// with TLS.
|
||||
RequireTLS bool
|
||||
|
||||
// The message envelope or message header contains UTF-8-encoded strings.
|
||||
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
|
||||
UTF8 bool
|
||||
|
||||
// The authorization identity asserted by the message sender in decoded
|
||||
// form with angle brackets stripped.
|
||||
//
|
||||
// nil value indicates missing AUTH, non-nil empty string indicates
|
||||
// AUTH=<>.
|
||||
//
|
||||
// Defined in RFC 4954.
|
||||
Auth *string
|
||||
NewSession(c *Conn) (Session, error)
|
||||
}
|
||||
|
||||
// Session is used by servers to respond to an SMTP client.
|
||||
@@ -68,17 +37,24 @@ type Session interface {
|
||||
// Free all resources associated with session.
|
||||
Logout() error
|
||||
|
||||
// Authenticate the user using SASL PLAIN.
|
||||
AuthPlain(username, password string) error
|
||||
|
||||
// Set return path for currently processed message.
|
||||
Mail(from string, opts MailOptions) error
|
||||
Mail(from string, opts *MailOptions) error
|
||||
// Add recipient for currently processed message.
|
||||
Rcpt(to string) error
|
||||
Rcpt(to string, opts *RcptOptions) error
|
||||
// Set currently processed message contents and send it.
|
||||
//
|
||||
// r must be consumed before Data returns.
|
||||
Data(r io.Reader) error
|
||||
}
|
||||
|
||||
// LMTPSession is an add-on interface for Session. It can be implemented by
|
||||
// LMTP servers to provide extra functionality.
|
||||
type LMTPSession interface {
|
||||
Session
|
||||
|
||||
// LMTPData is the LMTP-specific version of Data method.
|
||||
// It can be optionally implemented by the backend to provide
|
||||
// per-recipient status information when it is used over LMTP
|
||||
|
||||
314
vendor/github.com/emersion/go-smtp/client.go
generated
vendored
314
vendor/github.com/emersion/go-smtp/client.go
generated
vendored
@@ -21,15 +21,10 @@ import (
|
||||
|
||||
// A Client represents a client connection to an SMTP server.
|
||||
type Client struct {
|
||||
// Text is the textproto.Conn used by the Client. It is exported to allow for
|
||||
// clients to add extensions.
|
||||
Text *textproto.Conn
|
||||
|
||||
// keep a reference to the connection so it can be used to create a TLS
|
||||
// connection later
|
||||
conn net.Conn
|
||||
// whether the Client is using TLS
|
||||
tls bool
|
||||
conn net.Conn
|
||||
text *textproto.Conn
|
||||
serverName string
|
||||
lmtp bool
|
||||
// map of supported extensions
|
||||
@@ -50,15 +45,24 @@ type Client struct {
|
||||
DebugWriter io.Writer
|
||||
}
|
||||
|
||||
// Dial returns a new Client connected to an SMTP server at addr.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
// 30 seconds was chosen as it's the same duration as http.DefaultTransport's
|
||||
// timeout.
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
var defaultDialer = net.Dialer{Timeout: defaultTimeout}
|
||||
|
||||
// Dial returns a new Client connected to an SMTP server at addr. The addr must
|
||||
// include a port, as in "mail.example.com:smtp".
|
||||
//
|
||||
// This function returns a plaintext connection. To enable TLS, use StartTLS.
|
||||
func Dial(addr string) (*Client, error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
conn, err := defaultDialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
return NewClient(conn, host)
|
||||
client := NewClient(conn)
|
||||
client.serverName, _, _ = net.SplitHostPort(addr)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// DialTLS returns a new Client connected to an SMTP server via TLS at addr.
|
||||
@@ -66,20 +70,24 @@ func Dial(addr string) (*Client, error) {
|
||||
//
|
||||
// A nil tlsConfig is equivalent to a zero tls.Config.
|
||||
func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
tlsDialer := tls.Dialer{
|
||||
NetDialer: &defaultDialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
conn, err := tlsDialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
return NewClient(conn, host)
|
||||
client := NewClient(conn)
|
||||
client.serverName, _, _ = net.SplitHostPort(addr)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Client using an existing connection and host as a
|
||||
// server name to be used when authenticating.
|
||||
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
func NewClient(conn net.Conn) *Client {
|
||||
c := &Client{
|
||||
serverName: host,
|
||||
localName: "localhost",
|
||||
localName: "localhost",
|
||||
// As recommended by RFC 5321. For DATA command reply (3xx one) RFC
|
||||
// recommends a slightly shorter timeout but we do not bother
|
||||
// differentiating these.
|
||||
@@ -91,27 +99,15 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
|
||||
c.setConn(conn)
|
||||
|
||||
_, _, err := c.Text.ReadResponse(220)
|
||||
if err != nil {
|
||||
c.Text.Close()
|
||||
if protoErr, ok := err.(*textproto.Error); ok {
|
||||
return nil, toSMTPErr(protoErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
return c
|
||||
}
|
||||
|
||||
// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an
|
||||
// existing connector and host as a server name to be used when authenticating.
|
||||
func NewClientLMTP(conn net.Conn, host string) (*Client, error) {
|
||||
c, err := NewClient(conn, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// existing connection and host as a server name to be used when authenticating.
|
||||
func NewClientLMTP(conn net.Conn) *Client {
|
||||
c := NewClient(conn)
|
||||
c.lmtp = true
|
||||
return c, nil
|
||||
return c
|
||||
}
|
||||
|
||||
// setConn sets the underlying network connection for the client.
|
||||
@@ -139,23 +135,38 @@ func (c *Client) setConn(conn net.Conn) {
|
||||
Writer: w,
|
||||
Closer: conn,
|
||||
}
|
||||
c.Text = textproto.NewConn(rwc)
|
||||
|
||||
_, isTLS := conn.(*tls.Conn)
|
||||
c.tls = isTLS
|
||||
c.text = textproto.NewConn(rwc)
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Client) Close() error {
|
||||
return c.Text.Close()
|
||||
return c.text.Close()
|
||||
}
|
||||
|
||||
func (c *Client) greet() error {
|
||||
// Initial greeting timeout. RFC 5321 recommends 5 minutes.
|
||||
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
|
||||
defer c.conn.SetDeadline(time.Time{})
|
||||
|
||||
_, _, err := c.text.ReadResponse(220)
|
||||
if err != nil {
|
||||
c.text.Close()
|
||||
if protoErr, ok := err.(*textproto.Error); ok {
|
||||
return toSMTPErr(protoErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hello runs a hello exchange if needed.
|
||||
func (c *Client) hello() error {
|
||||
if !c.didHello {
|
||||
c.didHello = true
|
||||
err := c.ehlo()
|
||||
if err != nil {
|
||||
if err := c.greet(); err != nil {
|
||||
c.helloError = err
|
||||
} else if err := c.ehlo(); err != nil {
|
||||
c.helloError = c.helo()
|
||||
}
|
||||
}
|
||||
@@ -181,18 +192,18 @@ func (c *Client) Hello(localName string) error {
|
||||
}
|
||||
|
||||
// cmd is a convenience function that sends a command and returns the response
|
||||
// textproto.Error returned by c.Text.ReadResponse is converted into SMTPError.
|
||||
// textproto.Error returned by c.text.ReadResponse is converted into SMTPError.
|
||||
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
|
||||
defer c.conn.SetDeadline(time.Time{})
|
||||
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
id, err := c.text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
c.Text.StartResponse(id)
|
||||
defer c.Text.EndResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
c.text.StartResponse(id)
|
||||
defer c.text.EndResponse(id)
|
||||
code, msg, err := c.text.ReadResponse(expectCode)
|
||||
if err != nil {
|
||||
if protoErr, ok := err.(*textproto.Error); ok {
|
||||
smtpErr := toSMTPErr(protoErr)
|
||||
@@ -260,7 +271,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
||||
if config == nil {
|
||||
config = &tls.Config{}
|
||||
}
|
||||
if config.ServerName == "" {
|
||||
if config.ServerName == "" && c.serverName != "" {
|
||||
// Make a copy to avoid polluting argument
|
||||
config = config.Clone()
|
||||
config.ServerName = c.serverName
|
||||
@@ -365,34 +376,54 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmdStr := "MAIL FROM:<%s>"
|
||||
|
||||
var sb strings.Builder
|
||||
// A high enough power of 2 than 510+14+26+11+9+9+39+500
|
||||
sb.Grow(2048)
|
||||
fmt.Fprintf(&sb, "MAIL FROM:<%s>", from)
|
||||
if _, ok := c.ext["8BITMIME"]; ok {
|
||||
cmdStr += " BODY=8BITMIME"
|
||||
sb.WriteString(" BODY=8BITMIME")
|
||||
}
|
||||
if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 {
|
||||
cmdStr += " SIZE=" + strconv.Itoa(opts.Size)
|
||||
fmt.Fprintf(&sb, " SIZE=%v", opts.Size)
|
||||
}
|
||||
if opts != nil && opts.RequireTLS {
|
||||
if _, ok := c.ext["REQUIRETLS"]; ok {
|
||||
cmdStr += " REQUIRETLS"
|
||||
sb.WriteString(" REQUIRETLS")
|
||||
} else {
|
||||
return errors.New("smtp: server does not support REQUIRETLS")
|
||||
}
|
||||
}
|
||||
if opts != nil && opts.UTF8 {
|
||||
if _, ok := c.ext["SMTPUTF8"]; ok {
|
||||
cmdStr += " SMTPUTF8"
|
||||
sb.WriteString(" SMTPUTF8")
|
||||
} else {
|
||||
return errors.New("smtp: server does not support SMTPUTF8")
|
||||
}
|
||||
}
|
||||
if _, ok := c.ext["DSN"]; ok && opts != nil {
|
||||
switch opts.Return {
|
||||
case DSNReturnFull, DSNReturnHeaders:
|
||||
fmt.Fprintf(&sb, " RET=%s", string(opts.Return))
|
||||
case "":
|
||||
// This space is intentionally left blank
|
||||
default:
|
||||
return errors.New("smtp: Unknown RET parameter value")
|
||||
}
|
||||
if opts.EnvelopeID != "" {
|
||||
if !isPrintableASCII(opts.EnvelopeID) {
|
||||
return errors.New("smtp: Malformed ENVID parameter value")
|
||||
}
|
||||
fmt.Fprintf(&sb, " ENVID=%s", encodeXtext(opts.EnvelopeID))
|
||||
}
|
||||
}
|
||||
if opts != nil && opts.Auth != nil {
|
||||
if _, ok := c.ext["AUTH"]; ok {
|
||||
cmdStr += " AUTH=" + encodeXtext(*opts.Auth)
|
||||
fmt.Fprintf(&sb, " AUTH=%s", encodeXtext(*opts.Auth))
|
||||
}
|
||||
// We can safely discard parameter if server does not support AUTH.
|
||||
}
|
||||
_, _, err := c.cmd(250, cmdStr, from)
|
||||
_, _, err := c.cmd(250, "%s", sb.String())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -400,12 +431,53 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
|
||||
// A call to Rcpt must be preceded by a call to Mail and may be followed by
|
||||
// a Data call or another Rcpt call.
|
||||
//
|
||||
// If opts is not nil, RCPT arguments provided in the structure will be added
|
||||
// to the command. Handling of unsupported options depends on the extension.
|
||||
//
|
||||
// If server returns an error, it will be of type *SMTPError.
|
||||
func (c *Client) Rcpt(to string) error {
|
||||
func (c *Client) Rcpt(to string, opts *RcptOptions) error {
|
||||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil {
|
||||
|
||||
var sb strings.Builder
|
||||
// A high enough power of 2 than 510+29+501
|
||||
sb.Grow(2048)
|
||||
fmt.Fprintf(&sb, "RCPT TO:<%s>", to)
|
||||
if _, ok := c.ext["DSN"]; ok && opts != nil {
|
||||
if opts.Notify != nil && len(opts.Notify) != 0 {
|
||||
sb.WriteString(" NOTIFY=")
|
||||
if err := checkNotifySet(opts.Notify); err != nil {
|
||||
return errors.New("smtp: Malformed NOTIFY parameter value")
|
||||
}
|
||||
for i, v := range opts.Notify {
|
||||
if i != 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(string(v))
|
||||
}
|
||||
}
|
||||
if opts.OriginalRecipient != "" {
|
||||
var enc string
|
||||
switch opts.OriginalRecipientType {
|
||||
case DSNAddressTypeRFC822:
|
||||
if !isPrintableASCII(opts.OriginalRecipient) {
|
||||
return errors.New("smtp: Illegal address")
|
||||
}
|
||||
enc = encodeXtext(opts.OriginalRecipient)
|
||||
case DSNAddressTypeUTF8:
|
||||
if _, ok := c.ext["SMTPUTF8"]; ok {
|
||||
enc = encodeUTF8AddrUnitext(opts.OriginalRecipient)
|
||||
} else {
|
||||
enc = encodeUTF8AddrXtext(opts.OriginalRecipient)
|
||||
}
|
||||
default:
|
||||
return errors.New("smtp: Unknown address type")
|
||||
}
|
||||
fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc)
|
||||
}
|
||||
}
|
||||
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
c.rcpts = append(c.rcpts, to)
|
||||
@@ -416,10 +488,17 @@ type dataCloser struct {
|
||||
c *Client
|
||||
io.WriteCloser
|
||||
statusCb func(rcpt string, status *SMTPError)
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (d *dataCloser) Close() error {
|
||||
d.WriteCloser.Close()
|
||||
if d.closed {
|
||||
return fmt.Errorf("smtp: data writer closed twice")
|
||||
}
|
||||
|
||||
if err := d.WriteCloser.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout))
|
||||
defer d.c.conn.SetDeadline(time.Time{})
|
||||
@@ -428,7 +507,7 @@ func (d *dataCloser) Close() error {
|
||||
if d.c.lmtp {
|
||||
for expectedResponses > 0 {
|
||||
rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses]
|
||||
if _, _, err := d.c.Text.ReadResponse(250); err != nil {
|
||||
if _, _, err := d.c.text.ReadResponse(250); err != nil {
|
||||
if protoErr, ok := err.(*textproto.Error); ok {
|
||||
if d.statusCb != nil {
|
||||
d.statusCb(rcpt, toSMTPErr(protoErr))
|
||||
@@ -441,17 +520,18 @@ func (d *dataCloser) Close() error {
|
||||
}
|
||||
expectedResponses--
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
_, _, err := d.c.Text.ReadResponse(250)
|
||||
_, _, err := d.c.text.ReadResponse(250)
|
||||
if err != nil {
|
||||
if protoErr, ok := err.(*textproto.Error); ok {
|
||||
return toSMTPErr(protoErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
d.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data issues a DATA command to the server and returns a writer that
|
||||
@@ -465,7 +545,7 @@ func (c *Client) Data() (io.WriteCloser, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dataCloser{c, c.Text.DotWriter(), nil}, nil
|
||||
return &dataCloser{c: c, WriteCloser: c.text.DotWriter()}, nil
|
||||
}
|
||||
|
||||
// LMTPData is the LMTP-specific version of the Data method. It accepts a callback
|
||||
@@ -485,16 +565,51 @@ func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.Wri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dataCloser{c, c.Text.DotWriter(), statusCb}, nil
|
||||
return &dataCloser{c: c, WriteCloser: c.text.DotWriter(), statusCb: statusCb}, nil
|
||||
}
|
||||
|
||||
// SendMail will use an existing connection to send an email from
|
||||
// address from, to addresses to, with message r.
|
||||
//
|
||||
// This function does not start TLS, nor does it perform authentication. Use
|
||||
// StartTLS and Auth before-hand if desirable.
|
||||
//
|
||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||
//
|
||||
// The r parameter should be an RFC 822-style email with headers
|
||||
// first, a blank line, and then the message body. The lines of r
|
||||
// should be CRLF terminated. The r headers should usually include
|
||||
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||
// messages is accomplished by including an email address in the to
|
||||
// parameter but not including it in the r headers.
|
||||
func (c *Client) SendMail(from string, to []string, r io.Reader) error {
|
||||
var err error
|
||||
|
||||
if err = c.Mail(from, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||
|
||||
// SendMail connects to the server at addr, switches to TLS if
|
||||
// possible, authenticates with the optional mechanism a if possible,
|
||||
// and then sends an email from address from, to addresses to, with
|
||||
// message r.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
// SendMail connects to the server at addr, switches to TLS, authenticates with
|
||||
// the optional SASL client, and then sends an email from address from, to
|
||||
// addresses to, with message r. The addr must include a port, as in
|
||||
// "mail.example.com:smtp".
|
||||
//
|
||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||
//
|
||||
@@ -521,45 +636,65 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c, err := Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err = c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err = c.StartTLS(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); !ok {
|
||||
return errors.New("smtp: server doesn't support STARTTLS")
|
||||
}
|
||||
if a != nil && c.ext != nil {
|
||||
if _, ok := c.ext["AUTH"]; !ok {
|
||||
if err = c.StartTLS(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); !ok {
|
||||
return errors.New("smtp: server doesn't support AUTH")
|
||||
}
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from, nil); err != nil {
|
||||
if err := c.SendMail(from, to, r); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// SendMailTLS works like SendMail, but with implicit TLS.
|
||||
func SendMailTLS(addr string, a sasl.Client, from string, to []string, r io.Reader) error {
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, recp := range to {
|
||||
if err := validateLine(recp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
|
||||
c, err := DialTLS(addr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
defer c.Close()
|
||||
|
||||
if err = c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); !ok {
|
||||
return errors.New("smtp: server doesn't support AUTH")
|
||||
}
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := c.SendMail(from, to, r); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
@@ -616,7 +751,7 @@ func (c *Client) Quit() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Text.Close()
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func parseEnhancedCode(s string) (EnhancedCode, error) {
|
||||
@@ -639,9 +774,6 @@ func parseEnhancedCode(s string) (EnhancedCode, error) {
|
||||
// toSMTPErr converts textproto.Error into SMTPError, parsing
|
||||
// enhanced status code if it is present.
|
||||
func toSMTPErr(protoErr *textproto.Error) *SMTPError {
|
||||
if protoErr == nil {
|
||||
return nil
|
||||
}
|
||||
smtpErr := &SMTPError{
|
||||
Code: protoErr.Code,
|
||||
Message: protoErr.Msg,
|
||||
@@ -677,3 +809,11 @@ func (cdw clientDebugWriter) Write(b []byte) (int, error) {
|
||||
}
|
||||
return cdw.c.DebugWriter.Write(b)
|
||||
}
|
||||
|
||||
// validateLine checks to see if a line has CR or LF.
|
||||
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
|
||||
}
|
||||
|
||||
741
vendor/github.com/emersion/go-smtp/conn.go
generated
vendored
741
vendor/github.com/emersion/go-smtp/conn.go
generated
vendored
File diff suppressed because it is too large
Load Diff
21
vendor/github.com/emersion/go-smtp/data.go
generated
vendored
21
vendor/github.com/emersion/go-smtp/data.go
generated
vendored
@@ -2,6 +2,7 @@ package smtp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
@@ -29,7 +30,11 @@ var NoEnhancedCode = EnhancedCode{-1, -1, -1}
|
||||
var EnhancedCodeNotSet = EnhancedCode{0, 0, 0}
|
||||
|
||||
func (err *SMTPError) Error() string {
|
||||
return err.Message
|
||||
s := fmt.Sprintf("SMTP error %03d", err.Code)
|
||||
if err.Message != "" {
|
||||
s += ": " + err.Message
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (err *SMTPError) Temporary() bool {
|
||||
@@ -77,7 +82,7 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
|
||||
// not rewrite CRLF -> LF.
|
||||
|
||||
// Run data through a simple state machine to
|
||||
// elide leading dots and detect ending .\r\n line.
|
||||
// elide leading dots and detect End-of-Data (<CR><LF>.<CR><LF>) line.
|
||||
const (
|
||||
stateBeginLine = iota // beginning of line; initial state; must be zero
|
||||
stateDot // read . at beginning of line
|
||||
@@ -101,17 +106,16 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
|
||||
r.state = stateDot
|
||||
continue
|
||||
}
|
||||
if c == '\r' {
|
||||
r.state = stateCR
|
||||
break
|
||||
}
|
||||
r.state = stateData
|
||||
case stateDot:
|
||||
if c == '\r' {
|
||||
r.state = stateDotCR
|
||||
continue
|
||||
}
|
||||
if c == '\n' {
|
||||
r.state = stateEOF
|
||||
continue
|
||||
}
|
||||
|
||||
r.state = stateData
|
||||
case stateDotCR:
|
||||
if c == '\n' {
|
||||
@@ -129,9 +133,6 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
|
||||
if c == '\r' {
|
||||
r.state = stateCR
|
||||
}
|
||||
if c == '\n' {
|
||||
r.state = stateBeginLine
|
||||
}
|
||||
}
|
||||
b[n] = c
|
||||
n++
|
||||
|
||||
2
vendor/github.com/emersion/go-smtp/lengthlimit_reader.go
generated
vendored
2
vendor/github.com/emersion/go-smtp/lengthlimit_reader.go
generated
vendored
@@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrTooLongLine = errors.New("smtp: too longer line in input stream")
|
||||
var ErrTooLongLine = errors.New("smtp: too long a line in input stream")
|
||||
|
||||
// lineLimitReader reads from the underlying Reader but restricts
|
||||
// line length of lines in input stream to a certain length.
|
||||
|
||||
172
vendor/github.com/emersion/go-smtp/parse.go
generated
vendored
172
vendor/github.com/emersion/go-smtp/parse.go
generated
vendored
@@ -5,6 +5,14 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
|
||||
func cutPrefixFold(s, prefix string) (string, bool) {
|
||||
if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
|
||||
return "", false
|
||||
}
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
|
||||
func parseCmd(line string) (cmd string, arg string, err error) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
|
||||
@@ -15,36 +23,33 @@ func parseCmd(line string) (cmd string, arg string, err error) {
|
||||
case l == 0:
|
||||
return "", "", nil
|
||||
case l < 4:
|
||||
return "", "", fmt.Errorf("Command too short: %q", line)
|
||||
return "", "", fmt.Errorf("command too short: %q", line)
|
||||
case l == 4:
|
||||
return strings.ToUpper(line), "", nil
|
||||
case l == 5:
|
||||
// Too long to be only command, too short to have args
|
||||
return "", "", fmt.Errorf("Mangled command: %q", line)
|
||||
return "", "", fmt.Errorf("mangled command: %q", line)
|
||||
}
|
||||
|
||||
// If we made it here, command is long enough to have args
|
||||
if line[4] != ' ' {
|
||||
// There wasn't a space after the command?
|
||||
return "", "", fmt.Errorf("Mangled command: %q", line)
|
||||
return "", "", fmt.Errorf("mangled command: %q", line)
|
||||
}
|
||||
|
||||
// I'm not sure if we should trim the args or not, but we will for now
|
||||
//return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil
|
||||
return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil
|
||||
return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), nil
|
||||
}
|
||||
|
||||
// Takes the arguments proceeding a command and files them
|
||||
// into a map[string]string after uppercasing each key. Sample arg
|
||||
// string:
|
||||
// " BODY=8BITMIME SIZE=1024 SMTPUTF8"
|
||||
//
|
||||
// " BODY=8BITMIME SIZE=1024 SMTPUTF8"
|
||||
//
|
||||
// The leading space is mandatory.
|
||||
func parseArgs(args []string) (map[string]string, error) {
|
||||
func parseArgs(s string) (map[string]string, error) {
|
||||
argMap := map[string]string{}
|
||||
for _, arg := range args {
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
for _, arg := range strings.Fields(s) {
|
||||
m := strings.Split(arg, "=")
|
||||
switch len(m) {
|
||||
case 2:
|
||||
@@ -52,7 +57,7 @@ func parseArgs(args []string) (map[string]string, error) {
|
||||
case 1:
|
||||
argMap[strings.ToUpper(m[0])] = ""
|
||||
default:
|
||||
return nil, fmt.Errorf("Failed to parse arg string: %q", arg)
|
||||
return nil, fmt.Errorf("failed to parse arg string: %q", arg)
|
||||
}
|
||||
}
|
||||
return argMap, nil
|
||||
@@ -64,7 +69,146 @@ func parseHelloArgument(arg string) (string, error) {
|
||||
domain = arg[:idx]
|
||||
}
|
||||
if domain == "" {
|
||||
return "", fmt.Errorf("Invalid domain")
|
||||
return "", fmt.Errorf("invalid domain")
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// parser parses command arguments defined in RFC 5321 section 4.1.2.
|
||||
type parser struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (p *parser) peekByte() (byte, bool) {
|
||||
if len(p.s) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return p.s[0], true
|
||||
}
|
||||
|
||||
func (p *parser) readByte() (byte, bool) {
|
||||
ch, ok := p.peekByte()
|
||||
if ok {
|
||||
p.s = p.s[1:]
|
||||
}
|
||||
return ch, ok
|
||||
}
|
||||
|
||||
func (p *parser) acceptByte(ch byte) bool {
|
||||
got, ok := p.peekByte()
|
||||
if !ok || got != ch {
|
||||
return false
|
||||
}
|
||||
p.readByte()
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *parser) expectByte(ch byte) error {
|
||||
if !p.acceptByte(ch) {
|
||||
if len(p.s) == 0 {
|
||||
return fmt.Errorf("expected '%v', got EOF", string(ch))
|
||||
} else {
|
||||
return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0]))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) parseReversePath() (string, error) {
|
||||
if strings.HasPrefix(p.s, "<>") {
|
||||
p.s = strings.TrimPrefix(p.s, "<>")
|
||||
return "", nil
|
||||
}
|
||||
return p.parsePath()
|
||||
}
|
||||
|
||||
func (p *parser) parsePath() (string, error) {
|
||||
hasBracket := p.acceptByte('<')
|
||||
if p.acceptByte('@') {
|
||||
i := strings.IndexByte(p.s, ':')
|
||||
if i < 0 {
|
||||
return "", fmt.Errorf("malformed a-d-l")
|
||||
}
|
||||
p.s = p.s[i+1:]
|
||||
}
|
||||
mbox, err := p.parseMailbox()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("in mailbox: %v", err)
|
||||
}
|
||||
if hasBracket {
|
||||
if err := p.expectByte('>'); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return mbox, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseMailbox() (string, error) {
|
||||
localPart, err := p.parseLocalPart()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("in local-part: %v", err)
|
||||
} else if localPart == "" {
|
||||
return "", fmt.Errorf("local-part is empty")
|
||||
}
|
||||
|
||||
if err := p.expectByte('@'); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(localPart)
|
||||
sb.WriteByte('@')
|
||||
|
||||
for {
|
||||
ch, ok := p.peekByte()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ch == ' ' || ch == '\t' || ch == '>' {
|
||||
break
|
||||
}
|
||||
p.readByte()
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(sb.String(), "@") {
|
||||
return "", fmt.Errorf("domain is empty")
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (p *parser) parseLocalPart() (string, error) {
|
||||
var sb strings.Builder
|
||||
|
||||
if p.acceptByte('"') { // quoted-string
|
||||
for {
|
||||
ch, ok := p.readByte()
|
||||
switch ch {
|
||||
case '\\':
|
||||
ch, ok = p.readByte()
|
||||
case '"':
|
||||
return sb.String(), nil
|
||||
}
|
||||
if !ok {
|
||||
return "", fmt.Errorf("malformed quoted-string")
|
||||
}
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
} else { // dot-string
|
||||
for {
|
||||
ch, ok := p.peekByte()
|
||||
if !ok {
|
||||
return sb.String(), nil
|
||||
}
|
||||
switch ch {
|
||||
case '@':
|
||||
return sb.String(), nil
|
||||
case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t':
|
||||
return "", fmt.Errorf("malformed dot-string")
|
||||
}
|
||||
p.readByte()
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
vendor/github.com/emersion/go-smtp/server.go
generated
vendored
156
vendor/github.com/emersion/go-smtp/server.go
generated
vendored
@@ -1,6 +1,7 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -13,7 +14,9 @@ import (
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket")
|
||||
var (
|
||||
ErrServerClosed = errors.New("smtp: server already closed")
|
||||
)
|
||||
|
||||
// A function that creates SASL servers.
|
||||
type SaslServerFactory func(conn *Conn) sasl.Server
|
||||
@@ -26,20 +29,20 @@ type Logger interface {
|
||||
|
||||
// A SMTP server.
|
||||
type Server struct {
|
||||
// The type of network, "tcp" or "unix".
|
||||
Network string
|
||||
// TCP or Unix address to listen on.
|
||||
Addr string
|
||||
// The server TLS configuration.
|
||||
TLSConfig *tls.Config
|
||||
// Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a
|
||||
// TCP listener.
|
||||
// Enable LMTP mode, as defined in RFC 2033.
|
||||
LMTP bool
|
||||
|
||||
Domain string
|
||||
MaxRecipients int
|
||||
MaxMessageBytes int
|
||||
MaxMessageBytes int64
|
||||
MaxLineLength int
|
||||
AllowInsecureAuth bool
|
||||
Strict bool
|
||||
Debug io.Writer
|
||||
ErrorLog Logger
|
||||
ReadTimeout time.Duration
|
||||
@@ -57,6 +60,10 @@ type Server struct {
|
||||
// Should be used only if backend supports it.
|
||||
EnableBINARYMIME bool
|
||||
|
||||
// Advertise DSN (RFC 3461) capability.
|
||||
// Should be used only if backend supports it.
|
||||
EnableDSN bool
|
||||
|
||||
// If set, the AUTH command will not be advertised and authentication
|
||||
// attempts will be rejected. This setting overrides AllowInsecureAuth.
|
||||
AuthDisabled bool
|
||||
@@ -64,6 +71,8 @@ type Server struct {
|
||||
// The server backend.
|
||||
Backend Backend
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
caps []string
|
||||
auths map[string]SaslServerFactory
|
||||
done chan struct{}
|
||||
@@ -87,17 +96,15 @@ func NewServer(be Backend) *Server {
|
||||
sasl.Plain: func(conn *Conn) sasl.Server {
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
if identity != "" && identity != username {
|
||||
return errors.New("Identities not supported")
|
||||
return errors.New("identities not supported")
|
||||
}
|
||||
|
||||
state := conn.State()
|
||||
session, err := be.Login(&state, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
sess := conn.Session()
|
||||
if sess == nil {
|
||||
panic("No session when AUTH is called")
|
||||
}
|
||||
|
||||
conn.SetSession(session)
|
||||
return nil
|
||||
return sess.AuthPlain(username, password)
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -111,6 +118,8 @@ func (s *Server) Serve(l net.Listener) error {
|
||||
s.listeners = append(s.listeners, l)
|
||||
s.locker.Unlock()
|
||||
|
||||
var tempDelay time.Duration // how long to sleep on accept failure
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
@@ -119,11 +128,32 @@ func (s *Server) Serve(l net.Listener) error {
|
||||
// we called Close()
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
if tempDelay == 0 {
|
||||
tempDelay = 5 * time.Millisecond
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay)
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
go s.handleConn(newConn(c, s))
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
err := s.handleConn(newConn(c, s))
|
||||
if err != nil {
|
||||
s.ErrorLog.Printf("handler error: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +170,22 @@ func (s *Server) handleConn(c *Conn) error {
|
||||
s.locker.Unlock()
|
||||
}()
|
||||
|
||||
if tlsConn, ok := c.conn.(*tls.Conn); ok {
|
||||
if d := s.ReadTimeout; d != 0 {
|
||||
c.conn.SetReadDeadline(time.Now().Add(d))
|
||||
}
|
||||
if d := s.WriteTimeout; d != 0 {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(d))
|
||||
}
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.greet()
|
||||
|
||||
for {
|
||||
line, err := c.ReadLine()
|
||||
line, err := c.readLine()
|
||||
if err == nil {
|
||||
cmd, arg, err := parseCmd(line)
|
||||
if err != nil {
|
||||
@@ -153,34 +195,41 @@ func (s *Server) handleConn(c *Conn) error {
|
||||
|
||||
c.handle(cmd, arg)
|
||||
} else {
|
||||
if err == io.EOF {
|
||||
if err == io.EOF || errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
if err == ErrTooLongLine {
|
||||
c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
|
||||
c.writeResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
|
||||
c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye")
|
||||
c.writeResponse(421, EnhancedCode{4, 4, 2}, "Idle timeout, bye bye")
|
||||
return nil
|
||||
}
|
||||
|
||||
c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry")
|
||||
c.writeResponse(421, EnhancedCode{4, 4, 0}, "Connection error, sorry")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) network() string {
|
||||
if s.Network != "" {
|
||||
return s.Network
|
||||
}
|
||||
if s.LMTP {
|
||||
return "unix"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the network address s.Addr and then calls Serve
|
||||
// to handle requests on incoming connections.
|
||||
//
|
||||
// If s.Addr is blank and LMTP is disabled, ":smtp" is used.
|
||||
func (s *Server) ListenAndServe() error {
|
||||
network := "tcp"
|
||||
if s.LMTP {
|
||||
network = "unix"
|
||||
}
|
||||
network := s.network()
|
||||
|
||||
addr := s.Addr
|
||||
if !s.LMTP && addr == "" {
|
||||
@@ -198,18 +247,16 @@ func (s *Server) ListenAndServe() error {
|
||||
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls
|
||||
// Serve to handle requests on incoming TLS connections.
|
||||
//
|
||||
// If s.Addr is blank, ":smtps" is used.
|
||||
// If s.Addr is blank and LMTP is disabled, ":smtps" is used.
|
||||
func (s *Server) ListenAndServeTLS() error {
|
||||
if s.LMTP {
|
||||
return errTCPAndLMTP
|
||||
}
|
||||
network := s.network()
|
||||
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
if !s.LMTP && addr == "" {
|
||||
addr = ":smtps"
|
||||
}
|
||||
|
||||
l, err := tls.Listen("tcp", addr, s.TLSConfig)
|
||||
l, err := tls.Listen(network, addr, s.TLSConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -224,19 +271,19 @@ func (s *Server) ListenAndServeTLS() error {
|
||||
func (s *Server) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
return errors.New("smtp: server already closed")
|
||||
return ErrServerClosed
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
var err error
|
||||
s.locker.Lock()
|
||||
for _, l := range s.listeners {
|
||||
if lerr := l.Close(); lerr != nil && err == nil {
|
||||
err = lerr
|
||||
}
|
||||
}
|
||||
|
||||
s.locker.Lock()
|
||||
for conn := range s.conns {
|
||||
conn.Close()
|
||||
}
|
||||
@@ -245,6 +292,44 @@ func (s *Server) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server without interrupting any
|
||||
// active connections. Shutdown works by first closing all open
|
||||
// listeners and then waiting indefinitely for connections to return to
|
||||
// idle and then shut down.
|
||||
// If the provided context expires before the shutdown is complete,
|
||||
// Shutdown returns the context's error, otherwise it returns any
|
||||
// error returned from closing the Server's underlying Listener(s).
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
select {
|
||||
case <-s.done:
|
||||
return ErrServerClosed
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
var err error
|
||||
s.locker.Lock()
|
||||
for _, l := range s.listeners {
|
||||
if lerr := l.Close(); lerr != nil && err == nil {
|
||||
err = lerr
|
||||
}
|
||||
}
|
||||
s.locker.Unlock()
|
||||
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(connDone)
|
||||
s.wg.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-connDone:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// EnableAuth enables an authentication mechanism on this server.
|
||||
//
|
||||
// This function should not be called directly, it must only be used by
|
||||
@@ -252,12 +337,3 @@ func (s *Server) Close() error {
|
||||
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
|
||||
s.auths[name] = f
|
||||
}
|
||||
|
||||
// ForEachConn iterates through all opened connections.
|
||||
func (s *Server) ForEachConn(f func(*Conn)) {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
for conn := range s.conns {
|
||||
f(conn)
|
||||
}
|
||||
}
|
||||
|
||||
98
vendor/github.com/emersion/go-smtp/smtp.go
generated
vendored
98
vendor/github.com/emersion/go-smtp/smtp.go
generated
vendored
@@ -2,29 +2,93 @@
|
||||
//
|
||||
// It also implements the following extensions:
|
||||
//
|
||||
// 8BITMIME: RFC 1652
|
||||
// AUTH: RFC 2554
|
||||
// STARTTLS: RFC 3207
|
||||
// ENHANCEDSTATUSCODES: RFC 2034
|
||||
// SMTPUTF8: RFC 6531
|
||||
// REQUIRETLS: RFC 8689
|
||||
// CHUNKING: RFC 3030
|
||||
// BINARYMIME: RFC 3030
|
||||
// - 8BITMIME (RFC 1652)
|
||||
// - AUTH (RFC 2554)
|
||||
// - STARTTLS (RFC 3207)
|
||||
// - ENHANCEDSTATUSCODES (RFC 2034)
|
||||
// - SMTPUTF8 (RFC 6531)
|
||||
// - REQUIRETLS (RFC 8689)
|
||||
// - CHUNKING (RFC 3030)
|
||||
// - BINARYMIME (RFC 3030)
|
||||
// - DSN (RFC 3461, RFC 6533)
|
||||
//
|
||||
// LMTP (RFC 2033) is also supported.
|
||||
//
|
||||
// Additional extensions may be handled by other packages.
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
type BodyType string
|
||||
|
||||
const (
|
||||
Body7Bit BodyType = "7BIT"
|
||||
Body8BitMIME BodyType = "8BITMIME"
|
||||
BodyBinaryMIME BodyType = "BINARYMIME"
|
||||
)
|
||||
|
||||
// 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
|
||||
type DSNReturn string
|
||||
|
||||
const (
|
||||
DSNReturnFull DSNReturn = "FULL"
|
||||
DSNReturnHeaders DSNReturn = "HDRS"
|
||||
)
|
||||
|
||||
// MailOptions contains parameters for the MAIL command.
|
||||
type MailOptions struct {
|
||||
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
|
||||
Body BodyType
|
||||
|
||||
// Size of the body. Can be 0 if not specified by client.
|
||||
Size int64
|
||||
|
||||
// TLS is required for the message transmission.
|
||||
//
|
||||
// The message should be rejected if it can't be transmitted
|
||||
// with TLS.
|
||||
RequireTLS bool
|
||||
|
||||
// The message envelope or message header contains UTF-8-encoded strings.
|
||||
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
|
||||
UTF8 bool
|
||||
|
||||
// Value of RET= argument, FULL or HDRS.
|
||||
Return DSNReturn
|
||||
|
||||
// Envelope identifier set by the client.
|
||||
EnvelopeID string
|
||||
|
||||
// The authorization identity asserted by the message sender in decoded
|
||||
// form with angle brackets stripped.
|
||||
//
|
||||
// nil value indicates missing AUTH, non-nil empty string indicates
|
||||
// AUTH=<>.
|
||||
//
|
||||
// Defined in RFC 4954.
|
||||
Auth *string
|
||||
}
|
||||
|
||||
type DSNNotify string
|
||||
|
||||
const (
|
||||
DSNNotifyNever DSNNotify = "NEVER"
|
||||
DSNNotifyDelayed DSNNotify = "DELAY"
|
||||
DSNNotifyFailure DSNNotify = "FAILURE"
|
||||
DSNNotifySuccess DSNNotify = "SUCCESS"
|
||||
)
|
||||
|
||||
type DSNAddressType string
|
||||
|
||||
const (
|
||||
DSNAddressTypeRFC822 DSNAddressType = "RFC822"
|
||||
DSNAddressTypeUTF8 DSNAddressType = "UTF-8"
|
||||
)
|
||||
|
||||
// RcptOptions contains parameters for the RCPT command.
|
||||
type RcptOptions struct {
|
||||
// Value of NOTIFY= argument, NEVER or a combination of either of
|
||||
// DELAY, FAILURE, SUCCESS.
|
||||
Notify []DSNNotify
|
||||
|
||||
// Original recipient set by client.
|
||||
OriginalRecipientType DSNAddressType
|
||||
OriginalRecipient string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user