Skip to content

Commit c20b441

Browse files
foxcppemersion
authored andcommitted
client: Implement command timeouts
1 parent f804d2f commit c20b441

File tree

2 files changed

+32
-4
lines changed

2 files changed

+32
-4
lines changed

client.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"net/textproto"
1515
"strconv"
1616
"strings"
17+
"time"
1718

1819
"github.com/emersion/go-sasl"
1920
)
@@ -38,6 +39,12 @@ type Client struct {
3839
didHello bool // whether we've said HELO/EHLO/LHLO
3940
helloError error // the error from the hello
4041
rcpts []string // recipients accumulated for the current session
42+
43+
// Time to wait for command responses (this includes 3xx reply to DATA).
44+
CommandTimeout time.Duration
45+
46+
// Time to wait for responses after final dot.
47+
SubmissionTimeout time.Duration
4148
}
4249

4350
// Dial returns a new Client connected to an SMTP server at addr.
@@ -89,7 +96,20 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
8996
return nil, err
9097
}
9198
_, isTLS := conn.(*tls.Conn)
92-
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost", tls: isTLS}
99+
c := &Client{
100+
Text: text,
101+
conn: conn,
102+
serverName: host,
103+
localName: "localhost",
104+
tls: isTLS,
105+
// As recommended by RFC 5321. For DATA command reply (3xx one) RFC
106+
// recommends a slightly shorter timeout but we do not bother
107+
// differentiating these.
108+
CommandTimeout: 5 * time.Minute,
109+
// 10 minutes + 2 minute buffer in case the server is doing transparent
110+
// forwarding and also follows recommended timeouts.
111+
SubmissionTimeout: 12 * time.Minute,
112+
}
93113
return c, nil
94114
}
95115

@@ -142,6 +162,9 @@ func (c *Client) Hello(localName string) error {
142162
// cmd is a convenience function that sends a command and returns the response
143163
// textproto.Error returned by c.Text.ReadResponse is converted into SMTPError.
144164
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
165+
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
166+
defer c.conn.SetDeadline(time.Time{})
167+
145168
id, err := c.Text.Cmd(format, args...)
146169
if err != nil {
147170
return 0, "", err
@@ -174,6 +197,7 @@ func (c *Client) ehlo() error {
174197
if c.lmtp {
175198
cmd = "LHLO"
176199
}
200+
177201
_, msg, err := c.cmd(250, "%s %s", cmd, c.localName)
178202
if err != nil {
179203
return err
@@ -375,6 +399,10 @@ type dataCloser struct {
375399

376400
func (d *dataCloser) Close() error {
377401
d.WriteCloser.Close()
402+
403+
d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout))
404+
defer d.c.conn.SetDeadline(time.Time{})
405+
378406
expectedResponses := len(d.c.rcpts)
379407
if d.c.lmtp {
380408
for expectedResponses > 0 {

client_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestBasic(t *testing.T) {
8080
bcmdbuf := bufio.NewWriter(&cmdbuf)
8181
var fake faker
8282
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
83-
c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
83+
c := &Client{Text: textproto.NewConn(fake), conn: fake, localName: "localhost"}
8484

8585
if err := c.helo(); err != nil {
8686
t.Fatalf("HELO failed: %s", err)
@@ -912,7 +912,7 @@ func TestLMTP(t *testing.T) {
912912
bcmdbuf := bufio.NewWriter(&cmdbuf)
913913
var fake faker
914914
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
915-
c := &Client{Text: textproto.NewConn(fake), lmtp: true}
915+
c := &Client{Text: textproto.NewConn(fake), conn: fake, lmtp: true}
916916

917917
if err := c.Hello("localhost"); err != nil {
918918
t.Fatalf("LHLO failed: %s", err)
@@ -997,7 +997,7 @@ func TestLMTPData(t *testing.T) {
997997
bcmdbuf := bufio.NewWriter(&cmdbuf)
998998
var fake faker
999999
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
1000-
c := &Client{Text: textproto.NewConn(fake), lmtp: true}
1000+
c := &Client{Text: textproto.NewConn(fake), conn: fake, lmtp: true}
10011001

10021002
if err := c.Hello("localhost"); err != nil {
10031003
t.Fatalf("LHLO failed: %s", err)

0 commit comments

Comments
 (0)