comparison managesieve.go @ 0:6369453d47a3

Initial revision
author Guido Berhoerster <guido+managesieve@berhoerster.name>
date Thu, 15 Oct 2020 09:11:05 +0200
parents
children 8413916df2be
comparison
equal deleted inserted replaced
-1:000000000000 0:6369453d47a3
1 // Copyright (C) 2020 Guido Berhoerster <guido+managesieve@berhoerster.name>
2 //
3 // Permission is hereby granted, free of charge, to any person obtaining
4 // a copy of this software and associated documentation files (the
5 // "Software"), to deal in the Software without restriction, including
6 // without limitation the rights to use, copy, modify, merge, publish,
7 // distribute, sublicense, and/or sell copies of the Software, and to
8 // permit persons to whom the Software is furnished to do so, subject to
9 // the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included
12 // in all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 // Package managesieve implements the MANAGESIEVE protocol as specified in
23 // RFC 5804. It covers all mandatory parts of the protocol with the exception
24 // of the SCRAM-SHA-1 SASL mechanism. Additional SASL authentication
25 // mechanisms can be provided by consumers.
26 package managesieve
27
28 import (
29 "crypto/tls"
30 "encoding/base64"
31 "fmt"
32 "math"
33 "net"
34 "strconv"
35 "strings"
36 )
37
38 // ParserError represents a syntax error encountered while parsing a response
39 // from the server.
40 type ParserError string
41
42 func (e ParserError) Error() string {
43 return "parse error: " + string(e)
44 }
45
46 // ProtocolError represents a MANAGESIEVE protocol violation.
47 type ProtocolError string
48
49 func (e ProtocolError) Error() string {
50 return "protocol error: " + string(e)
51 }
52
53 // NotSupportedError is returned if an operation requires an extension that is
54 // not available.
55 type NotSupportedError string
56
57 func (e NotSupportedError) Error() string {
58 return "not supported: " + string(e)
59 }
60
61 // AuthenticationError is returned if an authentication attempt has failed.
62 type AuthenticationError string
63
64 func (e AuthenticationError) Error() string {
65 return "authentication failed: " + string(e)
66 }
67
68 // A ServerError is represents an error returned by the server in the form of a
69 // NO response.
70 type ServerError struct {
71 Code string // optional response code of the error
72 Msg string // optional human readable error message
73 }
74
75 func (e *ServerError) Error() string {
76 if e.Msg != "" {
77 return e.Msg
78 }
79 return "unspecified server error"
80 }
81
82 // The ConnClosedError is returned if the server has closed the connection.
83 type ConnClosedError struct {
84 Code string
85 Msg string
86 }
87
88 func (e *ConnClosedError) Error() string {
89 msg := "the server has closed to connection"
90 if e.Msg != "" {
91 return msg + ": " + e.Msg
92 }
93 return msg
94 }
95
96 // Tries to look up the MANAGESIEVE SRV record for the domain and returns an
97 // slice of strings containing hostnames and ports. If no SRV record was found
98 // it falls back to the given domain name and port 4190.
99 func LookupService(domain string) ([]string, error) {
100 _, addrs, err := net.LookupSRV("sieve", "tcp", domain)
101 if err != nil {
102 if dnserr, ok := err.(*net.DNSError); ok {
103 if dnserr.IsNotFound {
104 // no SRV record found, fall back to port 4190
105 services := [1]string{domain + ":4190"}
106 return services[:], nil
107 }
108 }
109 return nil, err
110 }
111 services := make([]string, len(addrs))
112 // addrs is already ordered by priority
113 for _, addr := range addrs {
114 services = append(services,
115 fmt.Sprintf("%s:%d", addr.Target, addr.Port))
116 }
117 return services, nil
118 }
119
120 // Checks whtether the given string conforms to the "Unicode Format for Network
121 // Interchange" specified in RFC 5198.
122 func IsNetUnicode(s string) bool {
123 for _, c := range s {
124 if c <= 0x1f || (c >= 0x7f && c <= 0x9f) ||
125 c == 0x2028 || c == 0x2029 {
126 return false
127 }
128 }
129 return true
130 }
131
132 func quoteString(s string) string {
133 return fmt.Sprintf("{%d+}\r\n%s", len(s), s)
134 }
135
136 // Client represents a client connection to a MANAGESIEVE server.
137 type Client struct {
138 conn net.Conn
139 p *parser
140 isAuth bool
141 capa map[string]string
142 serverName string
143 }
144
145 // Dial creates a new connection to a MANAGESIEVE server. The given addr must
146 // contain both a hostname or IP address and post.
147 func Dial(addr string) (*Client, error) {
148 host, _, err := net.SplitHostPort(addr)
149 if err != nil {
150 return nil, err
151 }
152 conn, err := net.Dial("tcp", addr)
153 if err != nil {
154 return nil, err
155 }
156 return NewClient(conn, host)
157 }
158
159 // NewClient create a new client based on an existing connection to a
160 // MANAGESIEVE server where host specifies the hostname of the remote end of
161 // the connection.
162 func NewClient(conn net.Conn, host string) (*Client, error) {
163 s := newScanner(conn)
164 p := &parser{s}
165 c := &Client{
166 conn: conn,
167 p: p,
168 serverName: host,
169 }
170
171 r, err := c.p.readReply()
172 if err != nil {
173 c.Close()
174 return nil, err
175 }
176 switch r.resp {
177 case responseOk:
178 c.capa, err = parseCapabilities(r)
179 case responseNo:
180 return c, &ServerError{r.code, r.msg}
181 case responseBye:
182 return c, &ConnClosedError{r.code, r.msg}
183 }
184 return c, err
185 }
186
187 // SupportsRFC5804 returns true if the server conforms to RFC 5804.
188 func (c *Client) SupportsRFC5804() bool {
189 _, ok := c.capa["VERSION"]
190 return ok
191 }
192
193 // SupportsTLS returns true if the server supports TLS connections via the
194 // STARTTLS command.
195 func (c *Client) SupportsTLS() bool {
196 _, ok := c.capa["STARTTLS"]
197 return ok
198 }
199
200 // Extensions returns the Sieve script extensions supported by the Sieve engine.
201 func (c *Client) Extensions() []string {
202 return strings.Fields(c.capa["SIEVE"])
203 }
204
205 // MaxRedirects returns the limit on the number of Sieve "redirect" during a
206 // single evaluation.
207 func (c *Client) MaxRedirects() int {
208 n, err := strconv.ParseUint(c.capa["MAXREDIRECTS"], 10, 32)
209 if err != nil {
210 return 0
211 }
212 return int(n)
213 }
214
215 // NotifyMethods returns the URI schema parts for supported notification
216 // methods.
217 func (c *Client) NotifyMethods() []string {
218 return strings.Fields(c.capa["NOTIFY"])
219 }
220
221 // SASLMechanisms returns the SASL authentication mechanism supported by the
222 // server. This may change depending on whether a TLS connection is used.
223 func (c *Client) SASLMechanisms() []string {
224 splitFunc := func(r rune) bool {
225 return r == ' '
226 }
227 return strings.FieldsFunc(c.capa["SASL"], splitFunc)
228 }
229
230 func (c *Client) cmd(args ...interface{}) (*reply, error) {
231 // write each arg separated by a space and terminated by CR+LF
232 for i, arg := range args {
233 if i > 0 {
234 if _, err := c.conn.Write([]byte{' '}); err != nil {
235 return nil, err
236 }
237 }
238 if _, err := fmt.Fprint(c.conn, arg); err != nil {
239 return nil, err
240 }
241 }
242 if _, err := c.conn.Write([]byte("\r\n")); err != nil {
243 return nil, err
244 }
245
246 r, err := c.p.readReply()
247 if err != nil {
248 return nil, err
249 }
250 if r.resp == responseNo {
251 return r, &ServerError{r.code, r.msg}
252 } else if r.resp == responseBye {
253 return r, &ConnClosedError{r.code, r.msg}
254 }
255 return r, nil
256 }
257
258 // StartTLS upgrades the connection to use TLS encryption based on the given
259 // configuration.
260 func (c *Client) StartTLS(config *tls.Config) error {
261 if _, ok := c.conn.(*tls.Conn); ok {
262 return ProtocolError("already using a TLS connection")
263 }
264 if c.isAuth {
265 return ProtocolError("cannot STARTTLS in authenticated state")
266 }
267 if _, ok := c.capa["STARTTLS"]; !ok {
268 return NotSupportedError("STARTTLS")
269 }
270 if _, err := c.cmd("STARTTLS"); err != nil {
271 return err
272 }
273 c.conn = tls.Client(c.conn, config)
274 s := newScanner(c.conn)
275 c.p = &parser{s}
276
277 r, err := c.p.readReply()
278 if err != nil {
279 return err
280 }
281 // capabilities are no longer valid if STARTTLS succeeded
282 c.capa, err = parseCapabilities(r)
283 return err
284 }
285
286 // TLSConnectionState returns the ConnectionState of the current TLS
287 // connection.
288 func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
289 tc, ok := c.conn.(*tls.Conn)
290 if !ok {
291 return
292 }
293 return tc.ConnectionState(), ok
294 }
295
296 // Authenticate authenticates a client using the given authentication
297 // mechanism. In case of an AuthenticationError the client remains in a defined
298 // state and can continue to be used.
299 func (c *Client) Authenticate(a Auth) error {
300 encoding := base64.StdEncoding
301 _, isTLS := c.conn.(*tls.Conn)
302 info := &ServerInfo{c.serverName, isTLS, c.SASLMechanisms()}
303 mech, resp, err := a.Start(info)
304 if err != nil {
305 return err
306 }
307 if _, err = fmt.Fprintf(c.conn, "AUTHENTICATE \"%s\" \"%s\"\r\n",
308 mech, encoding.EncodeToString(resp)); err != nil {
309 return err
310 }
311
312 var line []*token
313 // handle SASL challenge-response messages exchanged as base64-encoded
314 // strings until the server sends a MANAGESIEVE response which may
315 // contain some final SASL data
316 for {
317 line, err = c.p.readLine()
318 if err != nil {
319 return err
320 }
321
322 if c.p.isResponseLine(line) {
323 break
324 } else if len(line) != 1 ||
325 (line[0].typ != tokenQuotedString &&
326 line[0].typ != tokenLiteralString) {
327 return ParserError("failed to parse SASL data: expected string")
328 }
329 msg, err := encoding.DecodeString(line[0].literal)
330 if err != nil {
331 return ParserError("failed to decode SASL data: " +
332 err.Error())
333 }
334
335 // perform next step in authentication process
336 resp, authErr := a.Next(msg, true)
337 if authErr != nil {
338 // this error should be recoverable, abort
339 // authentication
340 if _, err := fmt.Fprintf(c.conn, "\"*\"\r\n"); err != nil {
341 return err
342 }
343
344 line, err = c.p.readLine()
345 if err != nil {
346 return err
347 }
348 if r, err := c.p.parseResponseLine(line); err != nil {
349 return err
350 } else if r.resp != responseNo {
351 return ProtocolError("invalid response to aborted authentication: expected NO")
352 }
353
354 return AuthenticationError(authErr.Error())
355 }
356
357 // send SASL response
358 if _, err := fmt.Fprintf(c.conn, "\"%s\"\r\n",
359 encoding.EncodeToString(resp)); err != nil {
360 return err
361 }
362 }
363
364 // handle MANAGESIEVE response
365 r, err := c.p.parseResponseLine(line)
366 if err != nil {
367 return err
368 }
369 if r.resp == responseNo {
370 return AuthenticationError(r.msg)
371 } else if r.resp == responseBye {
372 return &ConnClosedError{r.code, r.msg}
373 }
374
375 // check for SASL response code with final SASL data as the response
376 // code argument
377 if r.code == "SASL" {
378 if len(r.codeArgs) != 1 {
379 return ParserError("failed to parse SASL code argument: expected a single argument")
380 }
381 msg64 := r.codeArgs[0]
382 msg, err := encoding.DecodeString(msg64)
383 if err != nil {
384 return ParserError("failed to decode SASL code argument: " + err.Error())
385 }
386
387 if _, err = a.Next(msg, false); err != nil {
388 return AuthenticationError(err.Error())
389 }
390 }
391
392 // capabilities are no longer valid after succesful authentication
393 r, err = c.cmd("CAPABILITY")
394 if err != nil {
395 return err
396 }
397 c.capa, err = parseCapabilities(r)
398 return err
399 }
400
401 // HaveSpace queries the server if there is sufficient space to store a script
402 // with the given name and size. An already existing script with the same name
403 // will be treated as if it were replaced with a script of the given size.
404 func (c *Client) HaveSpace(name string, size int64) (bool, error) {
405 if size < 0 {
406 return false,
407 ProtocolError(fmt.Sprintf("invalid script size: %d",
408 size))
409 }
410 if size > math.MaxInt32 {
411 return false, ProtocolError("script exceeds maximum size")
412 }
413 r, err := c.cmd("HAVESPACE", quoteString(name), size)
414 if err != nil {
415 if r.code == "QUOTA" || r.code == "QUOTA/MAXSIZE" {
416 err = nil
417 }
418 }
419 return r.resp == responseOk, err
420 }
421
422 // PutScript stores the script content with the given name on the server. An
423 // already existing script with the same name will be replaced.
424 func (c *Client) PutScript(name, content string) error {
425 if !IsNetUnicode(name) {
426 return ProtocolError("script name must comply with Net-Unicode")
427 }
428 _, err := c.cmd("PUTSCRIPT", quoteString(name), quoteString(content))
429 return err
430 }
431
432 // ListScripts returns the names of all scripts on the server and the name of
433 // the currently active script. If there is no active script it returns the
434 // empty string.
435 func (c *Client) ListScripts() ([]string, string, error) {
436 r, err := c.cmd("LISTSCRIPTS")
437 if err != nil {
438 return nil, "", err
439 }
440
441 var scripts []string = make([]string, 0)
442 var active string
443 for _, tokens := range r.lines {
444 if tokens[0].typ != tokenQuotedString &&
445 tokens[0].typ != tokenLiteralString {
446 return nil, "", ParserError("failed to parse script list: expected string")
447 }
448 switch len(tokens) {
449 case 2:
450 if tokens[1].typ != tokenAtom ||
451 tokens[1].literal != "ACTIVE" {
452 return nil, "", ParserError("failed to parse script list: expected atom ACTIVE")
453 }
454 active = tokens[0].literal
455 fallthrough
456 case 1:
457 scripts = append(scripts, tokens[0].literal)
458 default:
459 return nil, "", ParserError("failed to parse script list: trailing data")
460 }
461 }
462 return scripts, active, nil
463 }
464
465 // ActivateScript activates a script. Only one script can be active at the same
466 // time, activating a script will deactivate the previously active script. If
467 // the name is the empty string the currently active script will be
468 // deactivated.
469 func (c *Client) ActivateScript(name string) error {
470 _, err := c.cmd("SETACTIVE", quoteString(name))
471 return err
472 }
473
474 // GetScript returns the content of the script with the given name.
475 func (c *Client) GetScript(name string) (string, error) {
476 r, err := c.cmd("GETSCRIPT", quoteString(name))
477 if err != nil {
478 return "", err
479 }
480 if len(r.lines) != 1 ||
481 (r.lines[0][0].typ != tokenQuotedString &&
482 r.lines[0][0].typ != tokenLiteralString) {
483 return "", ParserError("failed to parse script: expected string")
484 }
485 return r.lines[0][0].literal, nil
486 }
487
488 // DeleteScript deletes the script with the given name from the server.
489 func (c *Client) DeleteScript(name string) error {
490 _, err := c.cmd("DELETESCRIPT", quoteString(name))
491 return err
492 }
493
494 // RenameScript renames a script on the server. This operation is only
495 // available if the server conforms to RFC 5804.
496 func (c *Client) RenameScript(oldName, newName string) error {
497 if !c.SupportsRFC5804() {
498 return NotSupportedError("RENAMESCRIPT")
499 }
500 if !IsNetUnicode(newName) {
501 return ProtocolError("script name must comply with Net-Unicode")
502 }
503 _, err := c.cmd("RENAMESCRIPT", quoteString(oldName),
504 quoteString(newName))
505 return err
506 }
507
508 // CheckScript checks if the given script contains any errors. This operation
509 // is only available if the server conforms to RFC 5804.
510 func (c *Client) CheckScript(content string) error {
511 if !c.SupportsRFC5804() {
512 return NotSupportedError("CHECKSCRIPT")
513 }
514 _, err := c.cmd("CHECKSCRIPT", quoteString(content))
515 return err
516 }
517
518 // Noop does nothing but contact the server and can be used to prevent timeouts
519 // and to check whether the connection is still alive. This operation is only
520 // available if the server conforms to RFC 5804.
521 func (c *Client) Noop() error {
522 if !c.SupportsRFC5804() {
523 return NotSupportedError("NOOP")
524 }
525 _, err := c.cmd("NOOP")
526 return err
527 }
528
529 // Close closes the connection to the server immediately without informing the
530 // remote end that the client has finished. Under normal circumstances Logout
531 // should be used instead.
532 func (c *Client) Close() error {
533 return c.conn.Close()
534 }
535
536 // Logout first indicates to the server that the client is finished and
537 // subsequently closes the connection. No further commands can be sent after
538 // this.
539 func (c *Client) Logout() error {
540 _, err := c.cmd("LOGOUT")
541 cerr := c.Close()
542 if err == nil {
543 err = cerr
544 }
545 return err
546 }