Skip to content

Commit 6892256

Browse files
committed
imapclient: add COMPRESS
1 parent c3eb150 commit 6892256

File tree

4 files changed

+130
-12
lines changed

4 files changed

+130
-12
lines changed

imapclient/client.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,11 @@ func (c *Client) readResponse() error {
618618
token string
619619
err error
620620
startTLS *startTLSCommand
621+
compress *compressCommand
621622
)
622623
if tag != "" {
623624
token = "response-tagged"
624-
startTLS, err = c.readResponseTagged(tag, typ)
625+
startTLS, compress, err = c.readResponseTagged(tag, typ)
625626
} else {
626627
token = "response-data"
627628
err = c.readResponseData(typ)
@@ -637,6 +638,9 @@ func (c *Client) readResponse() error {
637638
if startTLS != nil {
638639
c.upgradeStartTLS(startTLS)
639640
}
641+
if compress != nil {
642+
c.upgradeCompress(compress)
643+
}
640644

641645
return nil
642646
}
@@ -666,10 +670,10 @@ func (c *Client) readContinueReq() error {
666670
return nil
667671
}
668672

669-
func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand, err error) {
673+
func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand, compress *compressCommand, err error) {
670674
cmd := c.deletePendingCmdByTag(tag)
671675
if cmd == nil {
672-
return nil, fmt.Errorf("received tagged response with unknown tag %q", tag)
676+
return nil, nil, fmt.Errorf("received tagged response with unknown tag %q", tag)
673677
}
674678

675679
// We've removed the command from the pending queue above. Make sure we
@@ -687,14 +691,14 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
687691
var code string
688692
if hasSP && c.dec.Special('[') { // resp-text-code
689693
if !c.dec.ExpectAtom(&code) {
690-
return nil, fmt.Errorf("in resp-text-code: %v", c.dec.Err())
694+
return nil, nil, fmt.Errorf("in resp-text-code: %v", c.dec.Err())
691695
}
692696
// TODO: LONGENTRIES and MAXSIZE from METADATA
693697
switch code {
694698
case "CAPABILITY": // capability-data
695699
caps, err := readCapabilities(c.dec)
696700
if err != nil {
697-
return nil, fmt.Errorf("in capability-data: %v", err)
701+
return nil, nil, fmt.Errorf("in capability-data: %v", err)
698702
}
699703
c.setCaps(caps)
700704
case "APPENDUID":
@@ -703,19 +707,19 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
703707
uid imap.UID
704708
)
705709
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectUID(&uid) {
706-
return nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err())
710+
return nil, nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err())
707711
}
708712
if cmd, ok := cmd.(*AppendCommand); ok {
709713
cmd.data.UID = uid
710714
cmd.data.UIDValidity = uidValidity
711715
}
712716
case "COPYUID":
713717
if !c.dec.ExpectSP() {
714-
return nil, c.dec.Err()
718+
return nil, nil, c.dec.Err()
715719
}
716720
uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec)
717721
if err != nil {
718-
return nil, fmt.Errorf("in resp-code-copy: %v", err)
722+
return nil, nil, fmt.Errorf("in resp-code-copy: %v", err)
719723
}
720724
if cmd, ok := cmd.(*CopyCommand); ok {
721725
cmd.data.UIDValidity = uidValidity
@@ -728,13 +732,13 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
728732
}
729733
}
730734
if !c.dec.ExpectSpecial(']') {
731-
return nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
735+
return nil, nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
732736
}
733737
hasSP = c.dec.SP()
734738
}
735739
var text string
736740
if hasSP && !c.dec.ExpectText(&text) {
737-
return nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
741+
return nil, nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
738742
}
739743

740744
var cmdErr error
@@ -748,14 +752,17 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
748752
Text: text,
749753
}
750754
default:
751-
return nil, fmt.Errorf("in resp-cond-state: expected OK, NO or BAD status condition, but got %v", typ)
755+
return nil, nil, fmt.Errorf("in resp-cond-state: expected OK, NO or BAD status condition, but got %v", typ)
752756
}
753757

754758
c.completeCommand(cmd, cmdErr)
755759

756760
if cmd, ok := cmd.(*startTLSCommand); ok && cmdErr == nil {
757761
startTLS = cmd
758762
}
763+
if cmd, ok := cmd.(*compressCommand); ok && cmdErr == nil {
764+
compress = cmd
765+
}
759766

760767
if cmdErr == nil && code != "CAPABILITY" {
761768
switch cmd.(type) {
@@ -765,7 +772,7 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
765772
}
766773
}
767774

768-
return startTLS, nil
775+
return startTLS, compress, nil
769776
}
770777

771778
func (c *Client) readResponseData(typ string) error {

imapclient/compress.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package imapclient
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"compress/flate"
7+
"io"
8+
)
9+
10+
// CompressOptions contains options for Client.Compress.
11+
type CompressOptions struct{}
12+
13+
// Compress enables connection-level compression.
14+
//
15+
// Unlike other commands, this method blocks until the command completes.
16+
//
17+
// A nil options pointer is equivalent to a zero options value.
18+
func (c *Client) Compress(options *CompressOptions) error {
19+
upgradeDone := make(chan struct{})
20+
cmd := &compressCommand{
21+
upgradeDone: upgradeDone,
22+
}
23+
enc := c.beginCommand("COMPRESS", cmd)
24+
enc.SP().Atom("DEFLATE")
25+
enc.flush()
26+
defer enc.end()
27+
28+
// The client MUST NOT send any further commands until it has seen the
29+
// result of COMPRESS.
30+
31+
if err := cmd.Wait(); err != nil {
32+
return err
33+
}
34+
35+
// The decoder goroutine will invoke Client.upgradeCompress
36+
<-upgradeDone
37+
return nil
38+
}
39+
40+
func (c *Client) upgradeCompress(compress *compressCommand) {
41+
defer close(compress.upgradeDone)
42+
43+
// Drain buffered data from our bufio.Reader
44+
var buf bytes.Buffer
45+
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
46+
panic(err) // unreachable
47+
}
48+
49+
conn := c.conn
50+
if c.tlsConn != nil {
51+
conn = c.tlsConn
52+
}
53+
54+
var r io.Reader
55+
if buf.Len() > 0 {
56+
r = io.MultiReader(&buf, conn)
57+
} else {
58+
r = c.conn
59+
}
60+
61+
w, err := flate.NewWriter(conn, flate.DefaultCompression)
62+
if err != nil {
63+
panic(err) // can only happen due to bad arguments
64+
}
65+
66+
rw := c.options.wrapReadWriter(struct {
67+
io.Reader
68+
io.Writer
69+
}{
70+
Reader: flate.NewReader(r),
71+
Writer: w,
72+
})
73+
74+
c.br.Reset(rw)
75+
// Unfortunately we can't re-use the bufio.Writer here, it races with
76+
// Client.Compress
77+
c.bw = bufio.NewWriter(rw)
78+
}
79+
80+
type compressCommand struct {
81+
cmd
82+
upgradeDone chan<- struct{}
83+
}

imapclient/compress_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package imapclient_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/emersion/go-imap/v2"
7+
)
8+
9+
func TestCompress(t *testing.T) {
10+
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
11+
defer client.Close()
12+
defer server.Close()
13+
14+
if algos := client.Caps().CompressAlgorithms(); len(algos) == 0 {
15+
t.Skipf("COMPRESS not supported")
16+
}
17+
18+
if err := client.Compress(nil); err != nil {
19+
t.Fatalf("Compress() = %v", err)
20+
}
21+
22+
if err := client.Noop().Wait(); err != nil {
23+
t.Fatalf("Noop().Wait() = %v", err)
24+
}
25+
}

response.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949

5050
// APPENDLIMIT
5151
ResponseCodeTooBig ResponseCode = "TOOBIG"
52+
53+
// COMPRESS
54+
ResponseCodeCompressionActive ResponseCode = "COMPRESSIONACTIVE"
5255
)
5356

5457
// StatusResponse is a generic status response.

0 commit comments

Comments
 (0)