Mercurial > projects > managesieve
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 } |