Skip to content

Commit 231c001

Browse files
committed
Merge support for MOVE extension
1 parent 75ea6d9 commit 231c001

File tree

8 files changed

+167
-8
lines changed

8 files changed

+167
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ includes:
136136
* [UNSELECT](https://tools.ietf.org/html/rfc3691)
137137
* [APPENDLIMIT](https://tools.ietf.org/html/rfc7889)
138138
* [ENABLE](https://tools.ietf.org/html/rfc5161)
139+
* [MOVE](https://tools.ietf.org/html/rfc6851)
139140

140141
Support for other extensions is provided via separate packages. See below.
141142

@@ -151,7 +152,6 @@ to learn how to use them.
151152
* [ID](https://github.com/ProtonMail/go-imap-id)
152153
* [IDLE](https://github.com/emersion/go-imap-idle)
153154
* [METADATA](https://github.com/emersion/go-imap-metadata)
154-
* [MOVE](https://github.com/emersion/go-imap-move)
155155
* [NAMESPACE](https://github.com/foxcpp/go-imap-namespace)
156156
* [QUOTA](https://github.com/emersion/go-imap-quota)
157157
* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread)

backend/move.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package backend
2+
3+
import (
4+
"github.com/emersion/go-imap"
5+
)
6+
7+
// MoveMailbox is a mailbox that supports moving messages.
8+
type MoveMailbox interface {
9+
Mailbox
10+
11+
// Move the specified message(s) to the end of the specified destination
12+
// mailbox. This means that a new message is created in the target mailbox
13+
// with a new UID, the original message is removed from the source mailbox,
14+
// and it appears to the client as a single action.
15+
//
16+
// If the destination mailbox does not exist, a server SHOULD return an error.
17+
// It SHOULD NOT automatically create the mailbox.
18+
MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error
19+
}

client/cmd_selected.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,73 @@ func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error {
272272
return c.copy(true, seqset, dest)
273273
}
274274

275+
func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error {
276+
if c.State() != imap.SelectedState {
277+
return ErrNoMailboxSelected
278+
}
279+
280+
if ok, err := c.Support("MOVE"); err != nil {
281+
return err
282+
} else if !ok {
283+
return c.moveFallback(uid, seqset, dest)
284+
}
285+
286+
var cmd imap.Commander = &commands.Move{
287+
SeqSet: seqset,
288+
Mailbox: dest,
289+
}
290+
if uid {
291+
cmd = &commands.Uid{Cmd: cmd}
292+
}
293+
294+
if status, err := c.Execute(cmd, nil); err != nil {
295+
return err
296+
} else {
297+
return status.Err()
298+
}
299+
}
300+
301+
// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support
302+
// MOVE.
303+
func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error {
304+
item := imap.FormatFlagsOp(imap.AddFlags, true)
305+
flags := []interface{}{imap.DeletedFlag}
306+
if uid {
307+
if err := c.UidCopy(seqset, dest); err != nil {
308+
return err
309+
}
310+
311+
if err := c.UidStore(seqset, item, flags, nil); err != nil {
312+
return err
313+
}
314+
} else {
315+
if err := c.Copy(seqset, dest); err != nil {
316+
return err
317+
}
318+
319+
if err := c.Store(seqset, item, flags, nil); err != nil {
320+
return err
321+
}
322+
}
323+
324+
return c.Expunge(nil)
325+
}
326+
327+
// Move moves the specified message(s) to the end of the specified destination
328+
// mailbox.
329+
//
330+
// If the server doesn't support the MOVE extension defined in RFC 6851,
331+
// go-imap will fallback to copy, store and expunge.
332+
func (c *Client) Move(seqset *imap.SeqSet, dest string) error {
333+
return c.move(false, seqset, dest)
334+
}
335+
336+
// UidMove is identical to Move, but seqset is interpreted as containing unique
337+
// identifiers instead of message sequence numbers.
338+
func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error {
339+
return c.move(true, seqset, dest)
340+
}
341+
275342
// Unselect frees server's resources associated with the selected mailbox and
276343
// returns the server to the authenticated state. This command performs the same
277344
// actions as Close, except that no messages are permanently removed from the

commands/move.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package commands
2+
3+
import (
4+
"errors"
5+
6+
"github.com/emersion/go-imap"
7+
"github.com/emersion/go-imap/utf7"
8+
)
9+
10+
// A MOVE command.
11+
// See RFC 6851 section 3.1.
12+
type Move struct {
13+
SeqSet *imap.SeqSet
14+
Mailbox string
15+
}
16+
17+
func (cmd *Move) Command() *imap.Command {
18+
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
19+
20+
return &imap.Command{
21+
Name: "MOVE",
22+
Arguments: []interface{}{cmd.SeqSet, mailbox},
23+
}
24+
}
25+
26+
func (cmd *Move) Parse(fields []interface{}) (err error) {
27+
if len(fields) < 2 {
28+
return errors.New("No enough arguments")
29+
}
30+
31+
seqset, ok := fields[0].(string)
32+
if !ok {
33+
return errors.New("Invalid sequence set")
34+
}
35+
if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
36+
return err
37+
}
38+
39+
mailbox, ok := fields[1].(string)
40+
if !ok {
41+
return errors.New("Mailbox name must be a string")
42+
}
43+
if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
44+
return err
45+
}
46+
47+
return
48+
}

server/cmd_selected.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55

66
"github.com/emersion/go-imap"
7+
"github.com/emersion/go-imap/backend"
78
"github.com/emersion/go-imap/commands"
89
"github.com/emersion/go-imap/responses"
910
)
@@ -294,6 +295,30 @@ func (cmd *Copy) UidHandle(conn Conn) error {
294295
return cmd.handle(true, conn)
295296
}
296297

298+
type Move struct {
299+
commands.Move
300+
}
301+
302+
func (h *Move) handle(uid bool, conn Conn) error {
303+
mailbox := conn.Context().Mailbox
304+
if mailbox == nil {
305+
return ErrNoMailboxSelected
306+
}
307+
308+
if m, ok := mailbox.(backend.MoveMailbox); ok {
309+
return m.MoveMessages(uid, h.SeqSet, h.Mailbox)
310+
}
311+
return errors.New("MOVE extension not supported")
312+
}
313+
314+
func (h *Move) Handle(conn Conn) error {
315+
return h.handle(false, conn)
316+
}
317+
318+
func (h *Move) UidHandle(conn Conn) error {
319+
return h.handle(true, conn)
320+
}
321+
297322
type Uid struct {
298323
commands.Uid
299324
}

server/conn.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (c *conn) Close() error {
163163
}
164164

165165
func (c *conn) Capabilities() []string {
166-
caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR", "CHILDREN", "UNSELECT"}
166+
caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR", "CHILDREN", "UNSELECT", "MOVE"}
167167

168168
appendLimitSet := false
169169
if c.ctx.State == imap.AuthenticatedState {

server/server.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ func New(bkd backend.Backend) *Server {
174174
hdlr.Subscribed = true
175175
return hdlr
176176
},
177-
"STATUS": func() Handler { return &Status{} },
178-
"APPEND": func() Handler { return &Append{} },
177+
"STATUS": func() Handler { return &Status{} },
178+
"APPEND": func() Handler { return &Append{} },
179+
"UNSELECT": func() Handler { return &Unselect{} },
179180

180181
"CHECK": func() Handler { return &Check{} },
181182
"CLOSE": func() Handler { return &Close{} },
@@ -184,9 +185,8 @@ func New(bkd backend.Backend) *Server {
184185
"FETCH": func() Handler { return &Fetch{} },
185186
"STORE": func() Handler { return &Store{} },
186187
"COPY": func() Handler { return &Copy{} },
188+
"MOVE": func() Handler { return &Move{} },
187189
"UID": func() Handler { return &Uid{} },
188-
189-
"UNSELECT": func() Handler { return &Unselect{} },
190190
}
191191

192192
return s
@@ -404,7 +404,7 @@ func (s *Server) Close() error {
404404
func (s *Server) Enable(extensions ...Extension) {
405405
for _, ext := range extensions {
406406
// Ignore built-in extensions
407-
if ext.Command("UNSELECT") != nil {
407+
if ext.Command("UNSELECT") != nil || ext.Command("MOVE") != nil {
408408
continue
409409
}
410410
s.extensions = append(s.extensions, ext)

server/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// Extnesions that are always advertised by go-imap server.
13-
const builtinExtensions = "LITERAL+ SASL-IR CHILDREN UNSELECT APPENDLIMIT"
13+
const builtinExtensions = "LITERAL+ SASL-IR CHILDREN UNSELECT MOVE APPENDLIMIT"
1414

1515
func testServer(t *testing.T) (s *server.Server, conn net.Conn) {
1616
bkd := memory.New()

0 commit comments

Comments
 (0)