This patch is the result of running Go 1.19's `gofmt` on the codebase, which automatically updates all Go doc comments to the new format. https://tip.golang.org/doc/go1.19#go-doc
158 lines
4.2 KiB
Go
158 lines
4.2 KiB
Go
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC
|
|
// 5321. It extends net/smtp as follows:
|
|
//
|
|
// - Supports SMTPUTF8, via MailAndRcpt.
|
|
// - Adds IsPermanent.
|
|
package smtp
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"net"
|
|
"net/smtp"
|
|
"net/textproto"
|
|
"unicode"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/envelope"
|
|
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
// A Client represents a client connection to an SMTP server.
|
|
type Client struct {
|
|
*smtp.Client
|
|
}
|
|
|
|
// NewClient uses the given connection to create a new Client.
|
|
func NewClient(conn net.Conn, host string) (*Client, error) {
|
|
c, err := smtp.NewClient(conn, host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wrap the textproto.Conn reader so we are not exposed to a memory
|
|
// exhaustion DoS on very long replies from the server.
|
|
// Limit to 2 MiB total (all replies through the lifetime of the client),
|
|
// which should be plenty for our uses of SMTP.
|
|
lr := &io.LimitedReader{R: c.Text.Reader.R, N: 2 * 1024 * 1024}
|
|
c.Text.Reader.R = bufio.NewReader(lr)
|
|
|
|
return &Client{c}, nil
|
|
}
|
|
|
|
// cmd sends a command and returns the response over the text connection.
|
|
// Based on Go's method of the same name.
|
|
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
|
id, err := c.Text.Cmd(format, args...)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
c.Text.StartResponse(id)
|
|
defer c.Text.EndResponse(id)
|
|
|
|
return c.Text.ReadResponse(expectCode)
|
|
}
|
|
|
|
// MailAndRcpt issues MAIL FROM and RCPT TO commands, in sequence.
|
|
// It will check the addresses, decide if SMTPUTF8 is needed, and apply the
|
|
// necessary transformations.
|
|
func (c *Client) MailAndRcpt(from string, to string) error {
|
|
from, fromNeeds, err := c.prepareForSMTPUTF8(from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
to, toNeeds, err := c.prepareForSMTPUTF8(to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
smtputf8Needed := fromNeeds || toNeeds
|
|
|
|
cmdStr := "MAIL FROM:<%s>"
|
|
if ok, _ := c.Extension("8BITMIME"); ok {
|
|
cmdStr += " BODY=8BITMIME"
|
|
}
|
|
if smtputf8Needed {
|
|
cmdStr += " SMTPUTF8"
|
|
}
|
|
_, _, err = c.cmd(250, cmdStr, from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = c.cmd(25, "RCPT TO:<%s>", to)
|
|
return err
|
|
}
|
|
|
|
// prepareForSMTPUTF8 prepares the address for SMTPUTF8.
|
|
// It returns:
|
|
// - The address to use. It is based on addr, and possibly modified to make
|
|
// it not need the extension, if the server does not support it.
|
|
// - Whether the address needs the extension or not.
|
|
// - An error if the address needs the extension, but the client does not
|
|
// support it.
|
|
func (c *Client) prepareForSMTPUTF8(addr string) (string, bool, error) {
|
|
// ASCII address pass through.
|
|
if isASCII(addr) {
|
|
return addr, false, nil
|
|
}
|
|
|
|
// Non-ASCII address also pass through if the server supports the
|
|
// extension.
|
|
// Note there's a chance the server wants the domain in IDNA anyway, but
|
|
// it could also require it to be UTF8. We assume that if it supports
|
|
// SMTPUTF8 then it knows what its doing.
|
|
if ok, _ := c.Extension("SMTPUTF8"); ok {
|
|
return addr, true, nil
|
|
}
|
|
|
|
// Something is not ASCII, and the server does not support SMTPUTF8:
|
|
// - If it's the local part, there's no way out and is required.
|
|
// - If it's the domain, use IDNA.
|
|
user, domain := envelope.Split(addr)
|
|
|
|
if !isASCII(user) {
|
|
return addr, true, &textproto.Error{Code: 599,
|
|
Msg: "local part is not ASCII but server does not support SMTPUTF8"}
|
|
}
|
|
|
|
// If it's only the domain, convert to IDNA and move on.
|
|
domain, err := idna.ToASCII(domain)
|
|
if err != nil {
|
|
// The domain is not IDNA compliant, which is odd.
|
|
// Fail with a permanent error, not ideal but this should not
|
|
// happen.
|
|
return addr, true, &textproto.Error{
|
|
Code: 599, Msg: "non-ASCII domain is not IDNA safe"}
|
|
}
|
|
|
|
return user + "@" + domain, false, nil
|
|
}
|
|
|
|
// isASCII returns true if all the characters in s are ASCII, false otherwise.
|
|
func isASCII(s string) bool {
|
|
for _, c := range s {
|
|
if c > unicode.MaxASCII {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsPermanent returns true if the error is permanent, and false otherwise.
|
|
// If it can't tell, it returns false.
|
|
func IsPermanent(err error) bool {
|
|
terr, ok := err.(*textproto.Error)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Error codes 5yz are permanent.
|
|
// https://tools.ietf.org/html/rfc5321#section-4.2.1
|
|
if terr.Code >= 500 && terr.Code < 600 {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|