aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-imap/client/cmd_auth.go
blob: a280017af481ced83f51cb2f387cde48683b23ca (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
368
369
370
371
372
373
374
375
376
377
378
379
380
package client

import (
	"errors"
	"time"

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

// ErrNotLoggedIn is returned if a function that requires the client to be
// logged in is called then the client isn't.
var ErrNotLoggedIn = errors.New("Not logged in")

func (c *Client) ensureAuthenticated() error {
	state := c.State()
	if state != imap.AuthenticatedState && state != imap.SelectedState {
		return ErrNotLoggedIn
	}
	return nil
}

// Select selects a mailbox so that messages in the mailbox can be accessed. Any
// currently selected mailbox is deselected before attempting the new selection.
// Even if the readOnly parameter is set to false, the server can decide to open
// the mailbox in read-only mode.
func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) {
	if err := c.ensureAuthenticated(); err != nil {
		return nil, err
	}

	cmd := &commands.Select{
		Mailbox:  name,
		ReadOnly: readOnly,
	}

	mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})}
	res := &responses.Select{
		Mailbox: mbox,
	}
	c.locker.Lock()
	c.mailbox = mbox
	c.locker.Unlock()

	status, err := c.execute(cmd, res)
	if err != nil {
		c.locker.Lock()
		c.mailbox = nil
		c.locker.Unlock()
		return nil, err
	}
	if err := status.Err(); err != nil {
		c.locker.Lock()
		c.mailbox = nil
		c.locker.Unlock()
		return nil, err
	}

	c.locker.Lock()
	mbox.ReadOnly = (status.Code == imap.CodeReadOnly)
	c.state = imap.SelectedState
	c.locker.Unlock()
	return mbox, nil
}

// Create creates a mailbox with the given name.
func (c *Client) Create(name string) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Create{
		Mailbox: name,
	}

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

// Delete permanently removes the mailbox with the given name.
func (c *Client) Delete(name string) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Delete{
		Mailbox: name,
	}

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

// Rename changes the name of a mailbox.
func (c *Client) Rename(existingName, newName string) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Rename{
		Existing: existingName,
		New:      newName,
	}

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

// Subscribe adds the specified mailbox name to the server's set of "active" or
// "subscribed" mailboxes.
func (c *Client) Subscribe(name string) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Subscribe{
		Mailbox: name,
	}

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

// Unsubscribe removes the specified mailbox name from the server's set of
// "active" or "subscribed" mailboxes.
func (c *Client) Unsubscribe(name string) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Unsubscribe{
		Mailbox: name,
	}

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

// List returns a subset of names from the complete set of all names available
// to the client.
//
// An empty name argument is a special request to return the hierarchy delimiter
// and the root name of the name given in the reference. The character "*" is a
// wildcard, and matches zero or more characters at this position. The
// character "%" is similar to "*", but it does not match a hierarchy delimiter.
func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error {
	defer close(ch)

	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.List{
		Reference: ref,
		Mailbox:   name,
	}
	res := &responses.List{Mailboxes: ch}

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

// Lsub returns a subset of names from the set of names that the user has
// declared as being "active" or "subscribed".
func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error {
	defer close(ch)

	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.List{
		Reference:  ref,
		Mailbox:    name,
		Subscribed: true,
	}
	res := &responses.List{
		Mailboxes:  ch,
		Subscribed: true,
	}

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

// Status requests the status of the indicated mailbox. It does not change the
// currently selected mailbox, nor does it affect the state of any messages in
// the queried mailbox.
//
// See RFC 3501 section 6.3.10 for a list of items that can be requested.
func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) {
	if err := c.ensureAuthenticated(); err != nil {
		return nil, err
	}

	cmd := &commands.Status{
		Mailbox: name,
		Items:   items,
	}
	res := &responses.Status{
		Mailbox: new(imap.MailboxStatus),
	}

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

// Append appends the literal argument as a new message to the end of the
// specified destination mailbox. This argument SHOULD be in the format of an
// RFC 2822 message. flags and date are optional arguments and can be set to
// nil and the empty struct.
func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error {
	if err := c.ensureAuthenticated(); err != nil {
		return err
	}

	cmd := &commands.Append{
		Mailbox: mbox,
		Flags:   flags,
		Date:    date,
		Message: msg,
	}

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

// Enable requests the server to enable the named extensions. The extensions
// which were successfully enabled are returned.
//
// See RFC 5161 section 3.1.
func (c *Client) Enable(caps []string) ([]string, error) {
	if ok, err := c.Support("ENABLE"); !ok || err != nil {
		return nil, ErrExtensionUnsupported
	}

	// ENABLE is invalid if a mailbox has been selected.
	if c.State() != imap.AuthenticatedState {
		return nil, ErrNotLoggedIn
	}

	cmd := &commands.Enable{Caps: caps}
	res := &responses.Enabled{}

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

func (c *Client) idle(stop <-chan struct{}) error {
	cmd := &commands.Idle{}

	res := &responses.Idle{
		Stop:      stop,
		RepliesCh: make(chan []byte, 10),
	}

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

// IdleOptions holds options for Client.Idle.
type IdleOptions struct {
	// LogoutTimeout is used to avoid being logged out by the server when
	// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
	// zero, a default is used. If negative, this behavior is disabled.
	LogoutTimeout time.Duration
	// Poll interval when the server doesn't support IDLE. If zero, a default
	// is used. If negative, polling is always disabled.
	PollInterval time.Duration
}

// Idle indicates to the server that the client is ready to receive unsolicited
// mailbox update messages. When the client wants to send commands again, it
// must first close stop.
//
// If the server doesn't support IDLE, go-imap falls back to polling.
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
	if ok, err := c.Support("IDLE"); err != nil {
		return err
	} else if !ok {
		return c.idleFallback(stop, opts)
	}

	logoutTimeout := 25 * time.Minute
	if opts != nil {
		if opts.LogoutTimeout > 0 {
			logoutTimeout = opts.LogoutTimeout
		} else if opts.LogoutTimeout < 0 {
			return c.idle(stop)
		}
	}

	t := time.NewTicker(logoutTimeout)
	defer t.Stop()

	for {
		stopOrRestart := make(chan struct{})
		done := make(chan error, 1)
		go func() {
			done <- c.idle(stopOrRestart)
		}()

		select {
		case <-t.C:
			close(stopOrRestart)
			if err := <-done; err != nil {
				return err
			}
		case <-stop:
			close(stopOrRestart)
			return <-done
		case err := <-done:
			close(stopOrRestart)
			if err != nil {
				return err
			}
		}
	}
}

func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
	pollInterval := time.Minute
	if opts != nil {
		if opts.PollInterval > 0 {
			pollInterval = opts.PollInterval
		} else if opts.PollInterval < 0 {
			return ErrExtensionUnsupported
		}
	}

	t := time.NewTicker(pollInterval)
	defer t.Stop()

	for {
		select {
		case <-t.C:
			if err := c.Noop(); err != nil {
				return err
			}
		case <-stop:
			return nil
		case <-c.LoggedOut():
			return errors.New("disconnected while idling")
		}
	}
}