Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server & Client: Implementing DSN extension (RFC 3461, RFC 6533) #233

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,34 +379,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 += fmt.Sprintf(" SIZE=%v", 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
}

Expand All @@ -422,7 +442,45 @@ 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)
Expand Down
81 changes: 77 additions & 4 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,9 +932,15 @@ Goodbye.`
}
}

var xtextClient = `MAIL FROM:<[email protected]> [email protected]
RCPT TO:<[email protected]> ORCPT=UTF-8;e\x{3D}[email protected]
`

func TestClientXtext(t *testing.T) {
server := "220 hello world\r\n" +
"200 some more"
"250 ok\r\n" +
"250 ok"
client := strings.Join(strings.Split(xtextClient, "\n"), "\r\n")
var wrote bytes.Buffer
var fake faker
fake.ReadWriter = struct {
Expand All @@ -949,11 +955,78 @@ func TestClientXtext(t *testing.T) {
t.Fatalf("NewClient: %v", err)
}
c.didHello = true
c.ext = map[string]string{"AUTH": "PLAIN"}
c.ext = map[string]string{"AUTH": "PLAIN", "DSN": ""}
email := "[email protected]"
c.Mail(email, &MailOptions{Auth: &email})
c.Rcpt(email, &RcptOptions{
OriginalRecipientType: DSNAddressTypeUTF8,
OriginalRecipient: email,
})
c.Close()
if got, want := wrote.String(), "MAIL FROM:<[email protected]> [email protected]\r\n"; got != want {
t.Errorf("wrote %q; want %q", got, want)
if got := wrote.String(); got != client {
t.Errorf("wrote %q; want %q", got, client)
}
}

const (
dsnEnvelopeID = "e=mc2"
dsnEmailRFC822 = "[email protected]"
dsnEmailUTF8 = "e=mc2@ドメイン名例.jp"
)

var dsnServer = `220 hello world
250 ok
250 ok
250 ok
250 ok
`

var dsnClient = `MAIL FROM:<[email protected]> RET=HDRS ENVID=e+3Dmc2
RCPT TO:<[email protected]> NOTIFY=NEVER ORCPT=RFC822;[email protected]
RCPT TO:<[email protected]> NOTIFY=FAILURE,DELAY ORCPT=UTF-8;e\x{3D}mc2@\x{30C9}\x{30E1}\x{30A4}\x{30F3}\x{540D}\x{4F8B}.jp
RCPT TO:<e=mc2@ドメイン名例.jp> ORCPT=UTF-8;e\x{3D}mc2@ドメイン名例.jp
`

func TestClientDSN(t *testing.T) {
server := strings.Join(strings.Split(dsnServer, "\n"), "\r\n")
client := strings.Join(strings.Split(dsnClient, "\n"), "\r\n")

var wrote bytes.Buffer
var fake faker
fake.ReadWriter = struct {
io.Reader
io.Writer
}{
strings.NewReader(server),
&wrote,
}
c, err := NewClient(fake, "fake.host")
if err != nil {
t.Fatalf("NewClient: %v", err)
}
c.didHello = true
c.ext = map[string]string{"DSN": ""}
c.Mail(dsnEmailRFC822, &MailOptions{
Return: DSNReturnHeaders,
EnvelopeID: dsnEnvelopeID,
})
c.Rcpt(dsnEmailRFC822, &RcptOptions{
OriginalRecipientType: DSNAddressTypeRFC822,
OriginalRecipient: dsnEmailRFC822,
Notify: []DSNNotify{DSNNotifyNever},
})
c.Rcpt(dsnEmailRFC822, &RcptOptions{
OriginalRecipientType: DSNAddressTypeUTF8,
OriginalRecipient: dsnEmailUTF8,
Notify: []DSNNotify{DSNNotifyFailure, DSNNotifyDelayed},
})
c.ext["SMTPUTF8"] = ""
c.Rcpt(dsnEmailUTF8, &RcptOptions{
OriginalRecipientType: DSNAddressTypeUTF8,
OriginalRecipient: dsnEmailUTF8,
})
c.Close()
if actualcmds := wrote.String(); client != actualcmds {
t.Errorf("wrote %q; want %q", actualcmds, client)
}
}
Loading