aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-imap/client/cmd_selected.go
blob: 0fb71ad9eccda9e63a5c77ede937c3750da429db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
package client

import (
	"errors"

	"github.com/emersion/go-imap"
	"github.com/emersion/go-imap/commands"
	"github.com/emersion/go-imap/responses"
)

var (
	// ErrNoMailboxSelected is returned if a command that requires a mailbox to be
	// selected is called when there isn't.
	ErrNoMailboxSelected = errors.New("No mailbox selected")

	// ErrExtensionUnsupported is returned if a command uses a extension that
	// is not supported by the server.
	ErrExtensionUnsupported = errors.New("The required extension is not supported by the server")
)

// Check requests a checkpoint of the currently selected mailbox. A checkpoint
// refers to any implementation-dependent housekeeping associated with the
// mailbox that is not normally executed as part of each command.
func (c *Client) Check() error {
	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	cmd := new(commands.Check)

	status, err := c.execute(cmd, nil)
	if err != nil {
		return err
	}

	return status.Err()
}

// Close permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox, and returns to the authenticated state from
// the selected state.
func (c *Client) Close() error {
	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	cmd := new(commands.Close)

	status, err := c.execute(cmd, nil)
	if err != nil {
		return err
	} else if err := status.Err(); err != nil {
		return err
	}

	c.locker.Lock()
	c.state = imap.AuthenticatedState
	c.mailbox = nil
	c.locker.Unlock()
	return nil
}

// Terminate closes the tcp connection
func (c *Client) Terminate() error {
	return c.conn.Close()
}

// Expunge permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox. If ch is not nil, sends sequence IDs of each
// deleted message to this channel.
func (c *Client) Expunge(ch chan uint32) error {
	if ch != nil {
		defer close(ch)
	}

	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	cmd := new(commands.Expunge)

	var h responses.Handler
	if ch != nil {
		h = &responses.Expunge{SeqNums: ch}
	}

	status, err := c.execute(cmd, h)
	if err != nil {
		return err
	}
	return status.Err()
}

func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) {
	if c.State() != imap.SelectedState {
		err = ErrNoMailboxSelected
		return
	}

	var cmd imap.Commander = &commands.Search{
		Charset:  charset,
		Criteria: criteria,
	}
	if uid {
		cmd = &commands.Uid{Cmd: cmd}
	}

	res := new(responses.Search)

	status, err = c.execute(cmd, res)
	if err != nil {
		return
	}

	err, ids = status.Err(), res.Ids
	return
}

func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) {
	ids, status, err := c.executeSearch(uid, criteria, "UTF-8")
	if status != nil && status.Code == imap.CodeBadCharset {
		// Some servers don't support UTF-8
		ids, _, err = c.executeSearch(uid, criteria, "US-ASCII")
	}
	return
}

// Search searches the mailbox for messages that match the given searching
// criteria. Searching criteria consist of one or more search keys. The response
// contains a list of message sequence IDs corresponding to those messages that
// match the searching criteria. When multiple keys are specified, the result is
// the intersection (AND function) of all the messages that match those keys.
// Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of
// searching criteria. When no criteria has been set, all messages in the mailbox
// will be searched using ALL criteria.
func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) {
	return c.search(false, criteria)
}

// UidSearch is identical to Search, but UIDs are returned instead of message
// sequence numbers.
func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) {
	return c.search(true, criteria)
}

func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
	defer close(ch)

	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	var cmd imap.Commander = &commands.Fetch{
		SeqSet: seqset,
		Items:  items,
	}
	if uid {
		cmd = &commands.Uid{Cmd: cmd}
	}

	res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}

	status, err := c.execute(cmd, res)
	if err != nil {
		return err
	}
	return status.Err()
}

// Fetch retrieves data associated with a message in the mailbox. See RFC 3501
// section 6.4.5 for a list of items that can be requested.
func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
	return c.fetch(false, seqset, items, ch)
}

// UidFetch is identical to Fetch, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
	return c.fetch(true, seqset, items, ch)
}

func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
	if ch != nil {
		defer close(ch)
	}

	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	// TODO: this could break extensions (this only works when item is FLAGS)
	if fields, ok := value.([]interface{}); ok {
		for i, field := range fields {
			if s, ok := field.(string); ok {
				fields[i] = imap.RawString(s)
			}
		}
	}

	// If ch is nil, the updated values are data which will be lost, so don't
	// retrieve it.
	if ch == nil {
		op, _, err := imap.ParseFlagsOp(item)
		if err == nil {
			item = imap.FormatFlagsOp(op, true)
		}
	}

	var cmd imap.Commander = &commands.Store{
		SeqSet: seqset,
		Item:   item,
		Value:  value,
	}
	if uid {
		cmd = &commands.Uid{Cmd: cmd}
	}

	var h responses.Handler
	if ch != nil {
		h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}
	}

	status, err := c.execute(cmd, h)
	if err != nil {
		return err
	}
	return status.Err()
}

// Store alters data associated with a message in the mailbox. If ch is not nil,
// the updated value of the data will be sent to this channel. See RFC 3501
// section 6.4.6 for a list of items that can be updated.
func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
	return c.store(false, seqset, item, value, ch)
}

// UidStore is identical to Store, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
	return c.store(true, seqset, item, value, ch)
}

func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error {
	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	var cmd imap.Commander = &commands.Copy{
		SeqSet:  seqset,
		Mailbox: dest,
	}
	if uid {
		cmd = &commands.Uid{Cmd: cmd}
	}

	status, err := c.execute(cmd, nil)
	if err != nil {
		return err
	}
	return status.Err()
}

// Copy copies the specified message(s) to the end of the specified destination
// mailbox.
func (c *Client) Copy(seqset *imap.SeqSet, dest string) error {
	return c.copy(false, seqset, dest)
}

// UidCopy is identical to Copy, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error {
	return c.copy(true, seqset, dest)
}

func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error {
	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	if ok, err := c.Support("MOVE"); err != nil {
		return err
	} else if !ok {
		return c.moveFallback(uid, seqset, dest)
	}

	var cmd imap.Commander = &commands.Move{
		SeqSet:  seqset,
		Mailbox: dest,
	}
	if uid {
		cmd = &commands.Uid{Cmd: cmd}
	}

	if status, err := c.Execute(cmd, nil); err != nil {
		return err
	} else {
		return status.Err()
	}
}

// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support
// MOVE.
func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error {
	item := imap.FormatFlagsOp(imap.AddFlags, true)
	flags := []interface{}{imap.DeletedFlag}
	if uid {
		if err := c.UidCopy(seqset, dest); err != nil {
			return err
		}

		if err := c.UidStore(seqset, item, flags, nil); err != nil {
			return err
		}
	} else {
		if err := c.Copy(seqset, dest); err != nil {
			return err
		}

		if err := c.Store(seqset, item, flags, nil); err != nil {
			return err
		}
	}

	return c.Expunge(nil)
}

// Move moves the specified message(s) to the end of the specified destination
// mailbox.
//
// If the server doesn't support the MOVE extension defined in RFC 6851,
// go-imap will fallback to copy, store and expunge.
func (c *Client) Move(seqset *imap.SeqSet, dest string) error {
	return c.move(false, seqset, dest)
}

// UidMove is identical to Move, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error {
	return c.move(true, seqset, dest)
}

// Unselect frees server's resources associated with the selected mailbox and
// returns the server to the authenticated state. This command performs the same
// actions as Close, except that no messages are permanently removed from the
// currently selected mailbox.
//
// If client does not support the UNSELECT extension, ErrExtensionUnsupported
// is returned.
func (c *Client) Unselect() error {
	if ok, err := c.Support("UNSELECT"); !ok || err != nil {
		return ErrExtensionUnsupported
	}

	if c.State() != imap.SelectedState {
		return ErrNoMailboxSelected
	}

	cmd := &commands.Unselect{}
	if status, err := c.Execute(cmd, nil); err != nil {
		return err
	} else if err := status.Err(); err != nil {
		return err
	}

	c.SetState(imap.AuthenticatedState, nil)
	return nil
}