forked from alienscience/imapsrv
/
command.go
298 lines (231 loc) · 6.71 KB
/
command.go
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
package imapsrv
import (
"crypto/tls"
"fmt"
"log"
"net/textproto"
"strings"
)
// command represents an IMAP command
type command interface {
// Execute the command and return an IMAP response
execute(s *session) *response
}
const (
// pathDelimiter is the delimiter used to distinguish between different folders
pathDelimiter = '/'
)
//------------------------------------------------------------------------------
// noop is a NOOP command
type noop struct {
tag string
}
// execute a NOOP command
func (c *noop) execute(s *session) *response {
return ok(c.tag, "NOOP Completed")
}
//------------------------------------------------------------------------------
// capability is a CAPABILITY command
type capability struct {
tag string
}
// execute a capability
func (c *capability) execute(s *session) *response {
var commands []string
switch s.listener.encryption {
case unencryptedLevel:
// TODO: do we want to support this?
case starttlsLevel:
if s.encryption == tlsLevel {
commands = append(commands, "AUTH=PLAIN")
} else {
commands = append(commands, "STARTTLS")
commands = append(commands, "LOGINDISABLED")
}
case tlsLevel:
commands = append(commands, "AUTH=PLAIN")
}
// Return all capabilities
return ok(c.tag, "CAPABILITY completed").
extra("CAPABILITY IMAP4rev1 " + strings.Join(commands, " "))
}
//------------------------------------------------------------------------------
type starttls struct {
tag string
}
func (c *starttls) execute(sess *session) *response {
sess.conn.Write([]byte(fmt.Sprintf("%s Begin TLS negotiation now", c.tag)))
sess.conn = tls.Server(sess.conn, &tls.Config{Certificates: sess.listener.certificates})
textConn := textproto.NewConn(sess.conn)
sess.encryption = tlsLevel
return empty().replaceBuffers(textConn)
}
//------------------------------------------------------------------------------
// login is a LOGIN command
type login struct {
tag string
userId string
password string
}
// execute a LOGIN command
func (c *login) execute(sess *session) *response {
// Has the user already logged in?
if sess.st != notAuthenticated {
message := "LOGIN already logged in"
sess.log(message)
return bad(c.tag, message)
}
auth, err := sess.server.config.authBackend.Authenticate(c.userId, c.password)
if auth {
sess.st = authenticated
return ok(c.tag, "LOGIN completed")
}
log.Println("Login request:", auth, err)
// Fail by default
return no(c.tag, "LOGIN failure")
}
//------------------------------------------------------------------------------
// logout is a LOGOUT command
type logout struct {
tag string
}
// execute a LOGOUT command
func (c *logout) execute(sess *session) *response {
sess.st = notAuthenticated
return ok(c.tag, "LOGOUT completed").
extra("BYE IMAP4rev1 Server logging out").
shouldClose()
}
//------------------------------------------------------------------------------
// selectMailbox is a SELECT command
type selectMailbox struct {
tag string
mailbox string
}
// execute a SELECT command
func (c *selectMailbox) execute(sess *session) *response {
// Is the user authenticated?
if sess.st != authenticated {
return mustAuthenticate(sess, c.tag, "SELECT")
}
// Select the mailbox
mbox := pathToSlice(c.mailbox)
exists, err := sess.selectMailbox(mbox)
if err != nil {
return internalError(sess, c.tag, "SELECT", err)
}
if !exists {
return no(c.tag, "SELECT No such mailbox")
}
// Build a response that includes mailbox information
res := ok(c.tag, "SELECT completed")
err = sess.addMailboxInfo(res)
if err != nil {
return internalError(sess, c.tag, "SELECT", err)
}
return res
}
//------------------------------------------------------------------------------
// list is a LIST command
type list struct {
tag string
reference string // Context of mailbox name
mboxPattern string // The mailbox name pattern
}
// execute a LIST command
func (c *list) execute(sess *session) *response {
// Is the user authenticated?
if sess.st != authenticated {
return mustAuthenticate(sess, c.tag, "LIST")
}
// Is the mailbox pattern empty? This indicates that we should return
// the delimiter and the root name of the reference
if c.mboxPattern == "" {
res := ok(c.tag, "LIST completed")
res.extra(fmt.Sprintf(`LIST () "%s" %s`, pathDelimiter, c.reference))
return res
}
// Convert the reference and mbox pattern into slices
ref := pathToSlice(c.reference)
mbox := pathToSlice(c.mboxPattern)
// Get the list of mailboxes
mboxes, err := sess.list(ref, mbox)
if err != nil {
return internalError(sess, c.tag, "LIST", err)
}
// Check for an empty response
if len(mboxes) == 0 {
return no(c.tag, "LIST no results")
}
// Respond with the mailboxes
res := ok(c.tag, "LIST completed")
for _, mbox := range mboxes {
res.extra(fmt.Sprintf(`LIST (%s) "%s" /%s`,
joinMailboxFlags(mbox),
string(pathDelimiter),
strings.Join(mbox.Path, string(pathDelimiter))))
}
return res
}
//------------------------------------------------------------------------------
// unknown is an unknown/unsupported command
type unknown struct {
tag string
cmd string
}
// execute reports an error for an unknown command
func (c *unknown) execute(s *session) *response {
message := fmt.Sprintf("%s unknown command", c.cmd)
s.log(message)
return bad(c.tag, message)
}
//------ Helper functions ------------------------------------------------------
// internalError logs an error and return an response
func internalError(sess *session, tag string, commandName string, err error) *response {
message := commandName + " " + err.Error()
sess.log(message)
return no(tag, message).shouldClose()
}
// mustAuthenticate indicates a command is invalid because the user has not authenticated
func mustAuthenticate(sess *session, tag string, commandName string) *response {
message := commandName + " not authenticated"
sess.log(message)
return bad(tag, message)
}
// pathToSlice converts a path to a slice of strings
func pathToSlice(path string) []string {
// Split the path
ret := strings.Split(path, string(pathDelimiter))
if len(ret) == 0 {
return ret
}
// Remove leading and trailing blanks
if ret[0] == "" {
if len(ret) > 1 {
ret = ret[1:]
} else {
return []string{}
}
}
lastIndex := len(ret) - 1
if ret[lastIndex] == "" {
if len(ret) > 1 {
ret = ret[0:lastIndex]
} else {
return []string{}
}
}
return ret
}
// joinMailboxFlags returns a string of mailbox flags for the given mailbox
func joinMailboxFlags(m *Mailbox) string {
// Convert the mailbox flags into a slice of strings
flags := make([]string, 0, 4)
for flag, str := range mailboxFlags {
if m.Flags&flag != 0 {
flags = append(flags, str)
}
}
// Return a joined string
return strings.Join(flags, ",")
}