├── LICENSE ├── README ├── imap-demo ├── demo1-gmail.txt ├── demo1.go ├── demo2-gmail.txt └── demo2.go ├── imap ├── client.go ├── client_test.go ├── command.go ├── command_test.go ├── doc.go ├── enum.go ├── example_test.go ├── field.go ├── field_test.go ├── imap.go ├── reader.go ├── reader_test.go ├── response.go ├── response_test.go ├── sasl.go ├── seqset.go ├── seqset_test.go ├── server.go ├── strings.go ├── strings_test.go ├── transport.go ├── transport_test.go ├── utf7.go ├── utf7_test.go └── util.go └── mock ├── mock.go ├── mock_test.go ├── net.go └── tls.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The Go-IMAP Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the 13 | distribution. 14 | 15 | * Neither the name of the go-imap project nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | IMAP4rev1 Client for Go 2 | ======================= 3 | 4 | To download and install this package run: 5 | 6 | go get github.com/mxk/go-imap/imap 7 | 8 | The documentation is available at: 9 | 10 | http://godoc.org/github.com/mxk/go-imap/imap 11 | http://godoc.org/github.com/mxk/go-imap/mock 12 | -------------------------------------------------------------------------------- /imap-demo/demo1-gmail.txt: -------------------------------------------------------------------------------- 1 | Connected to 74.125.131.109:993 (Tag=IZQEW) 2 | S: * OK Gimap ready for requests from 192.0.2.1 abc0123456789xyz.12 3 | Server greeting: Gimap ready for requests from 192.0.2.1 abc0123456789xyz.12 4 | C: IZQEW1 CAPABILITY 5 | S: * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH AUTH=XOAUTH2 6 | S: IZQEW1 OK Thats all she wrote! abc0123456789xyz.12 7 | C: IZQEW2 ID ("name" "goimap") 8 | S: * ID ("name" "GImap" "vendor" "Google, Inc." "support-url" "http://support.google.com/mail" "remote-host" "192.0.2.1" "connection-token" "abc0123456789xyz.12") 9 | S: IZQEW2 OK Success abc0123456789xyz.12 10 | --- ID --- 11 | 1 command response(s), 1 unilateral response(s) // Note: unilateral response is server greeting 12 | OK Success abc0123456789xyz.12 13 | 14 | C: IZQEW3 NOOP 15 | S: IZQEW3 OK Nothing Accomplished. abc0123456789xyz.12 16 | --- NOOP --- 17 | 0 command response(s), 0 unilateral response(s) 18 | OK Nothing Accomplished. abc0123456789xyz.12 19 | 20 | Raw logging disabled during LOGIN 21 | --- LOGIN --- 22 | 0 command response(s), 1 unilateral response(s) 23 | OK Go IMAP authenticated (Success) 24 | 25 | C: IZQEW6 GETQUOTAROOT "INBOX" 26 | S: * QUOTAROOT "INBOX" "" 27 | S: * QUOTA "" (STORAGE 9 26214400) 28 | S: IZQEW6 OK Success 29 | --- GETQUOTAROOT --- 30 | 2 command response(s), 0 unilateral response(s) 31 | OK Success 32 | 33 | C: IZQEW7 LIST "" "" 34 | S: * LIST (\Noselect) "/" "/" 35 | S: IZQEW7 OK Success 36 | --- LIST --- 37 | 1 command response(s), 0 unilateral response(s) 38 | OK Success 39 | 40 | C: IZQEW8 CREATE "GoIMAPxyz123/Demo1" 41 | S: IZQEW8 OK Success 42 | --- CREATE --- 43 | 0 command response(s), 0 unilateral response(s) 44 | OK Success 45 | 46 | C: IZQEW9 LIST "" "GoIMAPxyz123" 47 | S: * LIST (\Noselect \HasChildren) "/" "GoIMAPxyz123" 48 | S: IZQEW9 OK Success 49 | --- LIST --- 50 | 1 command response(s), 0 unilateral response(s) 51 | OK Success 52 | 53 | C: IZQEW10 LIST "" "GoIMAPxyz123/Demo1" 54 | S: * LIST (\HasNoChildren) "/" "GoIMAPxyz123/Demo1" 55 | S: IZQEW10 OK Success 56 | --- LIST --- 57 | 1 command response(s), 0 unilateral response(s) 58 | OK Success 59 | 60 | C: IZQEW11 RENAME "GoIMAPxyz123/Demo1" "GoIMAPxyz123/Demo12" 61 | S: IZQEW11 OK Success 62 | --- RENAME --- 63 | 0 command response(s), 0 unilateral response(s) 64 | OK Success 65 | 66 | C: IZQEW12 RENAME "GoIMAPxyz123/Demo12" "GoIMAPxyz123/Demo1" 67 | S: IZQEW12 OK Success 68 | --- RENAME --- 69 | 0 command response(s), 0 unilateral response(s) 70 | OK Success 71 | 72 | C: IZQEW13 SUBSCRIBE "GoIMAPxyz123/Demo1" 73 | S: IZQEW13 OK Success 74 | --- SUBSCRIBE --- 75 | 0 command response(s), 0 unilateral response(s) 76 | OK Success 77 | 78 | C: IZQEW14 UNSUBSCRIBE "GoIMAPxyz123/Demo1" 79 | S: IZQEW14 OK Success 80 | --- UNSUBSCRIBE --- 81 | 0 command response(s), 0 unilateral response(s) 82 | OK Success 83 | 84 | C: IZQEW15 STATUS "GoIMAPxyz123/Demo1" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN) 85 | S: * STATUS "GoIMAPxyz123/Demo1" (MESSAGES 0 RECENT 0 UIDNEXT 1 UIDVALIDITY 59 UNSEEN 0) 86 | S: IZQEW15 OK Success 87 | --- STATUS --- 88 | 1 command response(s), 0 unilateral response(s) 89 | OK Success 90 | 91 | C: IZQEW16 DELETE "GoIMAPxyz123/Demo1" 92 | S: IZQEW16 OK Success 93 | --- DELETE --- 94 | 0 command response(s), 0 unilateral response(s) 95 | OK Success 96 | 97 | C: IZQEW17 CREATE "GoIMAPxyz123/Demo1" 98 | S: IZQEW17 OK Success 99 | --- CREATE --- 100 | 0 command response(s), 0 unilateral response(s) 101 | OK Success 102 | 103 | C: IZQEW18 EXAMINE "GoIMAPxyz123/Demo1" 104 | S: * FLAGS (\Answered \Flagged \Draft \Deleted \Seen) 105 | S: * OK [PERMANENTFLAGS ()] Flags permitted. 106 | S: * OK [UIDVALIDITY 60] UIDs valid. 107 | S: * 0 EXISTS 108 | S: * 0 RECENT 109 | S: * OK [UIDNEXT 1] Predicted next UID. 110 | S: IZQEW18 OK [READ-ONLY] GoIMAPxyz123/Demo1 selected. (Success) 111 | --- EXAMINE --- 112 | 6 command response(s), 0 unilateral response(s) 113 | OK GoIMAPxyz123/Demo1 selected. (Success) 114 | 115 | C: IZQEW19 UNSELECT 116 | S: IZQEW19 OK Returned to authenticated state. (Success) 117 | --- UNSELECT --- 118 | 0 command response(s), 0 unilateral response(s) 119 | OK Returned to authenticated state. (Success) 120 | 121 | C: IZQEW20 APPEND "GoIMAPxyz123/Demo1" {70} 122 | S: + go ahead 123 | C: literal 70 bytes 124 | C: 125 | S: IZQEW20 OK [APPENDUID 60 1] (Success) 126 | --- APPEND --- 127 | 0 command response(s), 0 unilateral response(s) 128 | OK (Success) 129 | 130 | C: IZQEW21 SELECT "GoIMAPxyz123/Demo1" 131 | S: * FLAGS (\Answered \Flagged \Draft \Deleted \Seen) 132 | S: * OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen \*)] Flags permitted. 133 | S: * OK [UIDVALIDITY 60] UIDs valid. 134 | S: * 1 EXISTS 135 | S: * 0 RECENT 136 | S: * OK [UIDNEXT 2] Predicted next UID. 137 | S: IZQEW21 OK [READ-WRITE] GoIMAPxyz123/Demo1 selected. (Success) 138 | --- SELECT --- 139 | 6 command response(s), 0 unilateral response(s) 140 | OK GoIMAPxyz123/Demo1 selected. (Success) 141 | 142 | C: IZQEW22 CHECK 143 | S: IZQEW22 OK Success 144 | --- CHECK --- 145 | 0 command response(s), 0 unilateral response(s) 146 | OK Success 147 | 148 | --- "GoIMAPxyz123/Demo1" --- 149 | ReadOnly: false 150 | Flags: (\Answered \Deleted \Draft \Flagged \Seen) 151 | PermFlags: (\* \Answered \Deleted \Draft \Flagged \Seen) 152 | Messages: 1 153 | Recent: 0 154 | Unseen: 0 155 | UIDNext: 2 156 | UIDValidity: 60 157 | UIDNotSticky: false 158 | 159 | C: IZQEW23 UID SEARCH CHARSET UTF-8 SUBJECT "GoIMAP" 160 | S: * SEARCH 1 161 | S: IZQEW23 OK SEARCH completed (Success) 162 | --- UID SEARCH --- 163 | 1 command response(s), 0 unilateral response(s) 164 | OK SEARCH completed (Success) 165 | 166 | C: IZQEW24 FETCH 1 (FLAGS INTERNALDATE RFC822.SIZE BODY[]) 167 | S: * 1 FETCH (RFC822.SIZE 70 INTERNALDATE "10-Sep-2012 19:11:20 +0000" FLAGS (\Seen) BODY[] {70} 168 | S: literal 70 bytes 169 | S: ) 170 | S: IZQEW24 OK Success 171 | --- FETCH --- 172 | 1 command response(s), 0 unilateral response(s) 173 | OK Success 174 | 175 | C: IZQEW25 UID STORE 1 +FLAGS.SILENT (\Deleted) 176 | S: * 1 EXPUNGE 177 | S: * 0 EXISTS 178 | S: IZQEW25 OK Success 179 | --- UID STORE --- 180 | 0 command response(s), 2 unilateral response(s) 181 | OK Success 182 | 183 | C: IZQEW26 EXPUNGE 184 | S: IZQEW26 OK Success 185 | --- EXPUNGE --- 186 | 0 command response(s), 0 unilateral response(s) 187 | OK Success 188 | 189 | C: IZQEW27 UID SEARCH CHARSET UTF-8 SUBJECT "GoIMAP" 190 | S: * SEARCH 191 | S: IZQEW27 OK SEARCH completed (Success) 192 | --- UID SEARCH --- 193 | 1 command response(s), 0 unilateral response(s) 194 | OK SEARCH completed (Success) 195 | 196 | --- "GoIMAPxyz123/Demo1" --- 197 | ReadOnly: false 198 | Flags: (\Answered \Deleted \Draft \Flagged \Seen) 199 | PermFlags: (\* \Answered \Deleted \Draft \Flagged \Seen) 200 | Messages: 0 201 | Recent: 0 202 | Unseen: 0 203 | UIDNext: 2 204 | UIDValidity: 60 205 | UIDNotSticky: false 206 | 207 | C: IZQEW28 CLOSE 208 | S: IZQEW28 OK Returned to authenticated state. (Success) 209 | --- CLOSE --- 210 | 0 command response(s), 0 unilateral response(s) 211 | OK Returned to authenticated state. (Success) 212 | 213 | C: IZQEW29 DELETE "GoIMAPxyz123/Demo1" 214 | S: IZQEW29 OK Success 215 | --- DELETE --- 216 | 0 command response(s), 0 unilateral response(s) 217 | OK Success 218 | 219 | C: IZQEW30 LOGOUT 220 | S: * BYE LOGOUT Requested 221 | Logout reason: LOGOUT Requested 222 | S: IZQEW30 OK 73 good day (Success) 223 | S: (EOF) 224 | Close reason: end of stream 225 | Connection closing (flush=false) 226 | --- LOGOUT --- 227 | 1 command response(s), 0 unilateral response(s) 228 | OK 73 good day (Success) 229 | 230 | -------------------------------------------------------------------------------- /imap-demo/demo1.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build ignore 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | "github.com/mxk/go-imap/imap" 17 | ) 18 | 19 | const ( 20 | Addr = "imap.example.org" 21 | User = "user@example.org" 22 | Pass = "mypassword" 23 | MBox = "GoIMAPxyz123" 24 | ) 25 | 26 | const Msg = ` 27 | Subject: GoIMAP 28 | From: GoIMAP 29 | 30 | hello, world 31 | 32 | ` 33 | 34 | func main() { 35 | imap.DefaultLogger = log.New(os.Stdout, "", 0) 36 | imap.DefaultLogMask = imap.LogConn | imap.LogRaw 37 | 38 | c := Dial(Addr) 39 | defer func() { ReportOK(c.Logout(30 * time.Second)) }() 40 | 41 | if c.Caps["STARTTLS"] { 42 | ReportOK(c.StartTLS(nil)) 43 | } 44 | 45 | if c.Caps["ID"] { 46 | ReportOK(c.ID("name", "goimap")) 47 | } 48 | 49 | ReportOK(c.Noop()) 50 | ReportOK(Login(c, User, Pass)) 51 | 52 | if c.Caps["QUOTA"] { 53 | ReportOK(c.GetQuotaRoot("INBOX")) 54 | } 55 | 56 | cmd := ReportOK(c.List("", "")) 57 | delim := cmd.Data[0].MailboxInfo().Delim 58 | 59 | mbox := MBox + delim + "Demo1" 60 | if cmd, err := imap.Wait(c.Create(mbox)); err != nil { 61 | if rsp, ok := err.(imap.ResponseError); ok && rsp.Status == imap.NO { 62 | ReportOK(c.Delete(mbox)) 63 | } 64 | ReportOK(c.Create(mbox)) 65 | } else { 66 | ReportOK(cmd, err) 67 | } 68 | ReportOK(c.List("", MBox)) 69 | ReportOK(c.List("", mbox)) 70 | ReportOK(c.Rename(mbox, mbox+"2")) 71 | ReportOK(c.Rename(mbox+"2", mbox)) 72 | ReportOK(c.Subscribe(mbox)) 73 | ReportOK(c.Unsubscribe(mbox)) 74 | ReportOK(c.Status(mbox)) 75 | ReportOK(c.Delete(mbox)) 76 | 77 | ReportOK(c.Create(mbox)) 78 | ReportOK(c.Select(mbox, true)) 79 | ReportOK(c.Close(false)) 80 | 81 | msg := []byte(strings.Replace(Msg[1:], "\n", "\r\n", -1)) 82 | ReportOK(c.Append(mbox, nil, nil, imap.NewLiteral(msg))) 83 | 84 | ReportOK(c.Select(mbox, false)) 85 | ReportOK(c.Check()) 86 | 87 | fmt.Println(c.Mailbox) 88 | 89 | cmd = ReportOK(c.UIDSearch("SUBJECT", c.Quote("GoIMAP"))) 90 | set, _ := imap.NewSeqSet("") 91 | set.AddNum(cmd.Data[0].SearchResults()...) 92 | 93 | ReportOK(c.Fetch(set, "FLAGS", "INTERNALDATE", "RFC822.SIZE", "BODY[]")) 94 | ReportOK(c.UIDStore(set, "+FLAGS.SILENT", imap.NewFlagSet(`\Deleted`))) 95 | ReportOK(c.Expunge(nil)) 96 | ReportOK(c.UIDSearch("SUBJECT", c.Quote("GoIMAP"))) 97 | 98 | fmt.Println(c.Mailbox) 99 | 100 | ReportOK(c.Close(true)) 101 | ReportOK(c.Delete(mbox)) 102 | } 103 | 104 | func Dial(addr string) (c *imap.Client) { 105 | var err error 106 | if strings.HasSuffix(addr, ":993") { 107 | c, err = imap.DialTLS(addr, nil) 108 | } else { 109 | c, err = imap.Dial(addr) 110 | } 111 | if err != nil { 112 | panic(err) 113 | } 114 | return c 115 | } 116 | 117 | func Login(c *imap.Client, user, pass string) (cmd *imap.Command, err error) { 118 | defer c.SetLogMask(Sensitive(c, "LOGIN")) 119 | return c.Login(user, pass) 120 | } 121 | 122 | func Sensitive(c *imap.Client, action string) imap.LogMask { 123 | mask := c.SetLogMask(imap.LogConn) 124 | hide := imap.LogCmd | imap.LogRaw 125 | if mask&hide != 0 { 126 | c.Logln(imap.LogConn, "Raw logging disabled during", action) 127 | } 128 | c.SetLogMask(mask &^ hide) 129 | return mask 130 | } 131 | 132 | func ReportOK(cmd *imap.Command, err error) *imap.Command { 133 | var rsp *imap.Response 134 | if cmd == nil { 135 | fmt.Printf("--- ??? ---\n%v\n\n", err) 136 | panic(err) 137 | } else if err == nil { 138 | rsp, err = cmd.Result(imap.OK) 139 | } 140 | if err != nil { 141 | fmt.Printf("--- %s ---\n%v\n\n", cmd.Name(true), err) 142 | panic(err) 143 | } 144 | c := cmd.Client() 145 | fmt.Printf("--- %s ---\n"+ 146 | "%d command response(s), %d unilateral response(s)\n"+ 147 | "%s %s\n\n", 148 | cmd.Name(true), len(cmd.Data), len(c.Data), rsp.Status, rsp.Info) 149 | c.Data = nil 150 | return cmd 151 | } 152 | -------------------------------------------------------------------------------- /imap-demo/demo2-gmail.txt: -------------------------------------------------------------------------------- 1 | Connected to 173.194.68.109:993 (Tag=JUSQL) 2 | S: * OK Gimap ready for requests from 192.0.2.1 abc0123456789xyz.12 3 | Server greeting: Gimap ready for requests from 192.0.2.1 abc0123456789xyz.12 4 | C: JUSQL1 CAPABILITY 5 | S: * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH AUTH=XOAUTH2 6 | S: JUSQL1 OK Thats all she wrote! abc0123456789xyz.12 7 | C: JUSQL2 XYZZY 8 | S: JUSQL2 OK Nothing happens. abc0123456789xyz.12 9 | C: JUSQL3 LOGOUT 10 | S: * BYE Logout Requested abc0123456789xyz.12 11 | Logout reason: Logout Requested abc0123456789xyz.12 12 | S: JUSQL3 OK Quoth the raven, nevermore... abc0123456789xyz.12 13 | S: (EOF) 14 | Close reason: end of stream 15 | Connection closing (flush=false) 16 | -------------------------------------------------------------------------------- /imap-demo/demo2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build ignore 6 | 7 | package main 8 | 9 | import ( 10 | "log" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/mxk/go-imap/imap" 16 | ) 17 | 18 | const Addr = "imap.gmail.com:993" 19 | 20 | // CustomClient demonstrates how to add a new command to the IMAP client. 21 | type CustomClient struct{ *imap.Client } 22 | 23 | func NewCustomClient(c *imap.Client) CustomClient { 24 | c.CommandConfig["XYZZY"] = &imap.CommandConfig{States: imap.Login} 25 | return CustomClient{c} 26 | } 27 | 28 | func (c CustomClient) XYZZY() (cmd *imap.Command, err error) { 29 | if !c.Caps["XYZZY"] { 30 | return nil, imap.NotAvailableError("XYZZY") 31 | } 32 | return imap.Wait(c.Send("XYZZY")) 33 | } 34 | 35 | func main() { 36 | imap.DefaultLogger = log.New(os.Stdout, "", 0) 37 | imap.DefaultLogMask = imap.LogConn | imap.LogRaw 38 | 39 | c := NewCustomClient(Dial(Addr)) 40 | defer c.Logout(30 * time.Second) 41 | 42 | if _, err := c.XYZZY(); err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | func Dial(addr string) (c *imap.Client) { 48 | var err error 49 | if strings.HasSuffix(addr, ":993") { 50 | c, err = imap.DialTLS(addr, nil) 51 | } else { 52 | c, err = imap.Dial(addr) 53 | } 54 | if err != nil { 55 | panic(err) 56 | } 57 | return c 58 | } 59 | -------------------------------------------------------------------------------- /imap/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | // ErrAborted is returned by Command.Result when the command execution is 16 | // interrupted prior to receiving a completion response from the server. This is 17 | // usually caused by a break in the connection. 18 | var ErrAborted = errors.New("imap: command aborted") 19 | 20 | // abort is a sentinel value assigned to Command.result to indicate the absence 21 | // of a valid command completion response. 22 | var abort = new(Response) 23 | 24 | // Command represents a single command sent to the server. 25 | type Command struct { 26 | // FIFO queue for command data. These are the responses that were accepted 27 | // by this command's filter. New responses are appended to the end as they 28 | // are received. 29 | Data []*Response 30 | 31 | // Client that created this Command instance. 32 | client *Client 33 | 34 | // Command execution parameters copied from Client.CommandConfig. 35 | config CommandConfig 36 | 37 | // Command tag assigned by the Client. 38 | tag string 39 | 40 | // UID flag for FETCH, STORE, COPY, and SEARCH commands. 41 | uid bool 42 | 43 | // Command name without the UID prefix. 44 | name string 45 | 46 | // Sequence numbers or UIDs of messages affected by this command. This is 47 | // used to filter FETCH responses. 48 | seqset *SeqSet 49 | 50 | // Raw command text without CRLFs or literal strings. 51 | raw string 52 | 53 | // Command completion response. This is set to abort if the command is not 54 | // in progress, but a valid completion response was not received. 55 | result *Response 56 | } 57 | 58 | // newCommand initializes and returns a new Command instance. Nil is returned if 59 | // the specified name does not appear in c.CommandConfig. 60 | func newCommand(c *Client, name string) *Command { 61 | config := c.CommandConfig[name] 62 | if config == nil { 63 | return nil 64 | } 65 | cmd := &Command{client: c, config: *config, name: name, raw: name} 66 | if len(name) > 4 && name[:4] == "UID " { 67 | cmd.uid = true 68 | cmd.name = name[4:] 69 | } 70 | return cmd 71 | } 72 | 73 | // Client returns the Client instance that created this command. 74 | func (cmd *Command) Client() *Client { 75 | return cmd.client 76 | } 77 | 78 | // Tag returns the command tag assigned by the Client. 79 | func (cmd *Command) Tag() string { 80 | return cmd.tag 81 | } 82 | 83 | // UID returns true if the command is using UIDs instead of message sequence 84 | // numbers. 85 | func (cmd *Command) UID() bool { 86 | return cmd.uid 87 | } 88 | 89 | // Name returns the command name. If full == true, the UID prefix is included 90 | // for UID commands. 91 | func (cmd *Command) Name(full bool) string { 92 | if full && cmd.uid { 93 | return "UID " + cmd.name 94 | } 95 | return cmd.name 96 | } 97 | 98 | // InProgress returns true until the command completion result is available. No 99 | // new responses will be appended to cmd.Data after this method returns false. 100 | func (cmd *Command) InProgress() bool { 101 | return cmd.result == nil 102 | } 103 | 104 | // Result returns the command completion result. The call blocks until the 105 | // command is no longer in progress. If expect != 0, an error is returned if the 106 | // completion status is other than expected. ErrAborted is returned if the 107 | // command execution was interrupted prior to receiving a completion response. 108 | func (cmd *Command) Result(expect RespStatus) (rsp *Response, err error) { 109 | for cmd.result == nil { 110 | if err = cmd.client.Recv(block); err != nil { 111 | return 112 | } 113 | } 114 | if rsp = cmd.result; rsp == abort { 115 | rsp, err = nil, ErrAborted 116 | } else if expect != 0 && rsp.Status&expect == 0 { 117 | err = ResponseError{rsp, "unexpected completion status"} 118 | } 119 | return 120 | } 121 | 122 | // String returns the raw command text without CRLFs or literal data. 123 | func (cmd *Command) String() string { 124 | return cmd.raw 125 | } 126 | 127 | // rawCommand contains the raw text and literals about to be sent to the server. 128 | type rawCommand struct { 129 | *bytes.Buffer // Command text, including all required CRLFs 130 | 131 | literals []Literal // Literal strings 132 | nonsync bool // Support for non-synchronizing literals (RFC 2088) 133 | binary bool // Support for binary literals (RFC 3516) 134 | } 135 | 136 | // build returns a rawCommand struct constructed from the command parameters. 137 | func (cmd *Command) build(tag string, fields []Field) (*rawCommand, error) { 138 | raw := &rawCommand{ 139 | Buffer: bytes.NewBuffer(make([]byte, 0, 128)), 140 | nonsync: cmd.client.Caps["LITERAL+"], 141 | binary: cmd.client.Caps["BINARY"], 142 | } 143 | raw.WriteString(tag) 144 | raw.WriteByte(' ') 145 | if cmd.uid { 146 | raw.WriteString("UID ") 147 | } 148 | raw.WriteString(cmd.name) 149 | err := raw.WriteFields(fields, true) 150 | buf := raw.Bytes() 151 | raw.Write(crlf) 152 | 153 | if len(fields) > 0 { 154 | cmd.seqset, _ = fields[0].(*SeqSet) 155 | } 156 | if len(raw.literals) > 0 { 157 | buf = bytes.Replace(buf, crlf, nil, -1) 158 | } 159 | cmd.tag = tag 160 | cmd.raw = string(buf) 161 | return raw, err 162 | } 163 | 164 | // WriteFields writes command fields to the raw buffer using the appropriate 165 | // format for each field type. 166 | func (raw *rawCommand) WriteFields(fields []Field, SP bool) error { 167 | for _, f := range fields { 168 | if SP { 169 | raw.WriteByte(' ') 170 | } else { 171 | SP = true 172 | } 173 | switch v := f.(type) { 174 | case string: 175 | raw.WriteString(v) 176 | case int, int8, int16, int32, int64: 177 | raw.WriteString(strconv.FormatInt(intValue(f), 10)) 178 | case uint, uint8, uint16, uint32, uint64: 179 | raw.WriteString(strconv.FormatUint(uintValue(f), 10)) 180 | case time.Time: 181 | raw.WriteString(v.Format(DATETIME)) 182 | case []Field: 183 | raw.WriteByte('(') 184 | if err := raw.WriteFields(v, false); err != nil { 185 | return err 186 | } 187 | raw.WriteByte(')') 188 | case []byte: 189 | raw.Write(v) 190 | case Literal: 191 | info := v.Info() 192 | if info.Bin { 193 | if !raw.binary { 194 | return NotAvailableError("BINARY") 195 | } 196 | raw.WriteByte('~') 197 | } 198 | raw.WriteByte('{') 199 | raw.WriteString(strconv.FormatUint(uint64(info.Len), 10)) 200 | if raw.nonsync { 201 | raw.WriteByte('+') 202 | } 203 | raw.WriteString("}\r\n") 204 | raw.literals = append(raw.literals, v) 205 | case fmt.Stringer: 206 | raw.WriteString(v.String()) 207 | case nil: 208 | raw.WriteString("NIL") 209 | default: 210 | return fmt.Errorf("imap: invalid command field %#v", v) 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | // ReadLine returns the next line from the raw buffer, panicking if a complete 217 | // line is not found. The CRLF ending is stripped. The line remains valid until 218 | // the next read or write call. 219 | func (raw *rawCommand) ReadLine() []byte { 220 | b := raw.Bytes() 221 | n := bytes.IndexByte(b, '\n') + 1 222 | if n < 2 || b[n-2] != '\r' { 223 | panic("imap: corrupt command text buffer") // Should never happen... 224 | } 225 | return raw.Next(n)[:n-2] 226 | } 227 | 228 | // ResponseFilter defines the signature of functions that determine response 229 | // ownership. The function returns true if rsp belongs to cmd. A nil filter 230 | // rejects all responses. A response that is rejected by all active filters is 231 | // considered to be unilateral server data. 232 | type ResponseFilter func(cmd *Command, rsp *Response) bool 233 | 234 | // NameFilter accepts the response if rsp.Label matches the command name. 235 | func NameFilter(cmd *Command, rsp *Response) bool { 236 | return rsp.Label == cmd.name 237 | } 238 | 239 | // ByeFilter accepts the response if rsp.Status is BYE. 240 | func ByeFilter(_ *Command, rsp *Response) bool { 241 | return rsp.Status == BYE 242 | } 243 | 244 | // FetchFilter accepts FETCH and STORE command responses by matching message 245 | // sequence numbers or UIDs, depending on the command type. UID matches are more 246 | // exact because there is no risk of mistaking unilateral server data (e.g. an 247 | // unsolicited flags update) for command data. 248 | func FetchFilter(cmd *Command, rsp *Response) bool { 249 | msg := rsp.MessageInfo() 250 | if msg == nil { 251 | return false // Not a FETCH response 252 | } else if cmd.seqset == nil { 253 | return true // Accept all FETCH responses if SeqSet wasn't used 254 | } 255 | set := *cmd.seqset 256 | 257 | // Check message sequence number or UID against the set 258 | if cmd.uid { 259 | if msg.UID == 0 { 260 | return false // UID data item must be included for UID commands 261 | } else if set.Contains(msg.UID) { 262 | return true 263 | } 264 | } else if set.Contains(msg.Seq) { 265 | return true 266 | } 267 | 268 | // Try matching against "*" 269 | return set.Dynamic() && msg.Seq == cmd.client.Mailbox.Messages 270 | } 271 | 272 | // LabelFilter returns a new filter configured to accept responses with the 273 | // specified labels. 274 | func LabelFilter(labels ...string) ResponseFilter { 275 | accept := make(map[string]bool, len(labels)) 276 | for _, v := range labels { 277 | accept[v] = true 278 | } 279 | return func(_ *Command, rsp *Response) bool { 280 | return accept[rsp.Label] 281 | } 282 | } 283 | 284 | // SelectFilter accepts SELECT and EXAMINE command responses. 285 | var SelectFilter = LabelFilter( 286 | "FLAGS", "EXISTS", "RECENT", 287 | "UNSEEN", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", 288 | "UIDNOTSTICKY", 289 | ) 290 | 291 | // CommandConfig specifies command execution parameters. 292 | type CommandConfig struct { 293 | States ConnState // Mask of states in which this command may be issued 294 | Filter ResponseFilter // Filter for identifying command responses 295 | Exclusive bool // Exclusive Client access flag 296 | } 297 | 298 | // defaultCommands returns the default command configuration map used to 299 | // initialize Client.CommandConfig. 300 | func defaultCommands() map[string]*CommandConfig { 301 | const ( 302 | all = Login | Auth | Selected | Logout 303 | login = Login 304 | auth = Auth | Selected 305 | sel = Selected 306 | ) 307 | return map[string]*CommandConfig{ 308 | // RFC 3501 (6.1. Client Commands - Any State) 309 | "CAPABILITY": &CommandConfig{States: all, Filter: NameFilter}, 310 | "NOOP": &CommandConfig{States: all}, 311 | "LOGOUT": &CommandConfig{States: all, Filter: ByeFilter}, 312 | 313 | // RFC 3501 (6.2. Client Commands - Not Authenticated State) 314 | "STARTTLS": &CommandConfig{States: login, Exclusive: true}, 315 | "AUTHENTICATE": &CommandConfig{States: login, Exclusive: true}, 316 | "LOGIN": &CommandConfig{States: login, Exclusive: true}, 317 | 318 | // RFC 3501 (6.3. Client Commands - Authenticated State) 319 | "SELECT": &CommandConfig{States: auth, Filter: SelectFilter, Exclusive: true}, 320 | "EXAMINE": &CommandConfig{States: auth, Filter: SelectFilter, Exclusive: true}, 321 | "CREATE": &CommandConfig{States: auth}, 322 | "DELETE": &CommandConfig{States: auth}, 323 | "RENAME": &CommandConfig{States: auth}, 324 | "SUBSCRIBE": &CommandConfig{States: auth}, 325 | "UNSUBSCRIBE": &CommandConfig{States: auth}, 326 | "LIST": &CommandConfig{States: auth, Filter: NameFilter}, 327 | "LSUB": &CommandConfig{States: auth, Filter: NameFilter}, 328 | "STATUS": &CommandConfig{States: auth, Filter: NameFilter}, 329 | "APPEND": &CommandConfig{States: auth}, 330 | 331 | // RFC 3501 (6.4. Client Commands - Selected State) 332 | "CHECK": &CommandConfig{States: sel}, 333 | "CLOSE": &CommandConfig{States: sel, Exclusive: true}, 334 | "EXPUNGE": &CommandConfig{States: sel, Filter: NameFilter}, 335 | "SEARCH": &CommandConfig{States: sel, Filter: NameFilter}, 336 | "FETCH": &CommandConfig{States: sel, Filter: FetchFilter}, 337 | "STORE": &CommandConfig{States: sel, Filter: FetchFilter}, 338 | "COPY": &CommandConfig{States: sel}, 339 | "UID SEARCH": &CommandConfig{States: sel, Filter: NameFilter}, 340 | "UID FETCH": &CommandConfig{States: sel, Filter: FetchFilter}, 341 | "UID STORE": &CommandConfig{States: sel, Filter: FetchFilter}, 342 | "UID COPY": &CommandConfig{States: sel}, 343 | 344 | // RFC 2087 345 | "SETQUOTA": &CommandConfig{States: auth, Filter: LabelFilter("QUOTA")}, 346 | "GETQUOTA": &CommandConfig{States: auth, Filter: LabelFilter("QUOTA")}, 347 | "GETQUOTAROOT": &CommandConfig{States: auth, Filter: LabelFilter("QUOTA", "QUOTAROOT")}, 348 | 349 | // RFC 2177 350 | "IDLE": &CommandConfig{States: auth, Exclusive: true}, 351 | 352 | // RFC 2971 353 | "ID": &CommandConfig{States: all, Filter: NameFilter}, 354 | 355 | // RFC 3691 356 | "UNSELECT": &CommandConfig{States: sel, Exclusive: true}, 357 | 358 | // RFC 4315 359 | "UID EXPUNGE": &CommandConfig{States: sel, Filter: NameFilter}, 360 | 361 | // RFC 4978 362 | "COMPRESS": &CommandConfig{States: auth, Exclusive: true}, 363 | 364 | // RFC 5161 365 | "ENABLE": &CommandConfig{States: all, Filter: LabelFilter("ENABLED")}, 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /imap/command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func newSeqSet(set string) *SeqSet { 14 | s, _ := NewSeqSet(set) 15 | return s 16 | } 17 | 18 | func TestCommand(t *testing.T) { 19 | tests := []struct { 20 | tag string 21 | name string 22 | fields []Field 23 | out *Command 24 | }{ 25 | {"A001", "CAPABILITY", nil, &Command{ 26 | name: "CAPABILITY", 27 | tag: "A001", 28 | raw: `A001 CAPABILITY`}}, 29 | 30 | {"", "setCaps", []Field{"IMAP4rev1"}, nil}, 31 | {"A001", "LOGIN", []Field{`"username"`, `"password"`}, &Command{ 32 | name: "LOGIN", 33 | tag: "A001", 34 | raw: `A001 LOGIN "username" "password"`}}, 35 | {"A002", "LOGIN", []Field{lit(`username`), `"password"`}, &Command{ 36 | name: "LOGIN", 37 | tag: "A002", 38 | raw: `A002 LOGIN {8} "password"`}}, 39 | {"A003", "LOGIN", []Field{`"username"`, lit(`password`)}, &Command{ 40 | name: "LOGIN", 41 | tag: "A003", 42 | raw: `A003 LOGIN "username" {8}`}}, 43 | {"A004", "LOGIN", []Field{lit(`username`), lit(`password`)}, &Command{ 44 | name: "LOGIN", 45 | tag: "A004", 46 | raw: `A004 LOGIN {8} {8}`}}, 47 | 48 | {"", "setCaps", []Field{"IMAP4rev1", "LITERAL+"}, nil}, 49 | {"A005", "LOGIN", []Field{lit(`username`), lit(`password`)}, &Command{ 50 | name: "LOGIN", 51 | tag: "A005", 52 | raw: `A005 LOGIN {8+} {8+}`}}, 53 | 54 | {"", "setCaps", []Field{"IMAP4rev1", "LITERAL+", "BINARY"}, nil}, 55 | {"A006", "LOGIN", []Field{lit(`username`), lit8(`password`)}, &Command{ 56 | name: "LOGIN", 57 | tag: "A006", 58 | raw: `A006 LOGIN {8+} ~{8+}`}}, 59 | 60 | {"A001", "FETCH", []Field{newSeqSet("1,2,3,4"), []Field{"FAST"}}, &Command{ 61 | name: "FETCH", 62 | seqset: newSeqSet("1:4"), 63 | tag: "A001", 64 | raw: `A001 FETCH 1:4 (FAST)`}}, 65 | 66 | {"A001", "UID FETCH", []Field{newSeqSet("1,3:*"), []Field{"BODY[]", "UID"}}, &Command{ 67 | uid: true, 68 | name: "FETCH", 69 | seqset: newSeqSet("1,3:*"), 70 | tag: "A001", 71 | raw: `A001 UID FETCH 1,3:* (BODY[] UID)`}}, 72 | 73 | {"A001", "CHECK", []Field{"str", 123, []Field{[]Field(nil), []byte("data")}, nil, NewFlagSet(`\Answered`, `\Flagged`)}, &Command{ 74 | name: "CHECK", 75 | tag: "A001", 76 | raw: `A001 CHECK str 123 (() data) NIL (\Answered \Flagged)`}}, 77 | {"A002", "CHECK", []Field{time.Date(1986, time.February, 1, 23, 0, 1, 0, time.UTC)}, &Command{ 78 | name: "CHECK", 79 | tag: "A002", 80 | raw: `A002 CHECK " 1-Feb-1986 23:00:01 +0000"`}}, 81 | } 82 | c := &Client{ 83 | Caps: make(map[string]bool), 84 | CommandConfig: defaultCommands(), 85 | debugLog: newDebugLog(nil, LogNone), 86 | } 87 | for _, test := range tests { 88 | if test.name == "setCaps" { 89 | c.setCaps(test.fields) 90 | continue 91 | } 92 | out := newCommand(c, test.name) 93 | if out != nil { 94 | out.config = CommandConfig{} 95 | } 96 | if test.out != nil { 97 | test.out.client = c 98 | } 99 | if _, err := out.build(test.tag, test.fields); err != nil { 100 | t.Errorf("build(%s %s) unexpected error; %v", test.tag, test.name, err) 101 | } else if !reflect.DeepEqual(out, test.out) { 102 | t.Errorf("build(%s %s) expected\n%#v; got\n%#v", test.tag, test.name, test.out, out) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /imap/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package imap implements an IMAP4rev1 client, as defined in RFC 3501. 7 | 8 | The implementation provides low-level access to all protocol features described 9 | in the relevant RFCs (see list below), and assumes that the developer is 10 | familiar with the basic rules governing connection states, command execution, 11 | server responses, and IMAP data types. Reading this documentation alone is not 12 | sufficient for writing a working IMAP client. As a starting point, you should 13 | read RFC 2683 to understand some of the nuances of the protocol operation. 14 | 15 | The rest of the documentation deals with the implementation of this package and 16 | not the protocol in general. 17 | 18 | Introduction 19 | 20 | The package provides three main objects for interacting with an IMAP4 server: 21 | Client, Command, and Response. The client sends commands to the server and 22 | receives responses. The Response object is capable of representing all possible 23 | server responses and provides helper methods for decoding common data formats, 24 | such as LIST, FETCH, SEARCH, etc. 25 | 26 | The client has two interfaces for issuing commands. The Send method is the raw 27 | command interface that can be used for implementing new commands, which are not 28 | already supported by the package. All standard commands, as well as those from a 29 | number of popular extensions, have dedicated methods that perform capability and 30 | field type checks, and properly encode the command arguments before passing them 31 | to Send. 32 | 33 | Response Delivery 34 | 35 | To support execution of multiple concurrent commands, each server response goes 36 | through a filtering process to identify its owner. Each command in progress has 37 | an associated ResponseFilter function for this purpose. The Client calls all 38 | active filters in the command-issue order until one of the filters "claims" the 39 | response. Claimed responses are appended to the Command.Data queue of the 40 | claimer. Responses rejected by all filters are referred to as "unilateral server 41 | data" and are appended to the Client.Data queue. Commands documented as 42 | expecting "no specific responses" use a nil ResponseFilter, which never claims 43 | anything. Thus, responses for commands such as NOOP are always delivered to 44 | Client.Data queue. 45 | 46 | The Client/Command state can only be updated by a call to Client.Recv. Each call 47 | receives and delivers at most one response, but these calls are often implicit, 48 | such as when using the Wait helper function (see below). Be sure to inspect and 49 | clear out the Client data queue after all receive operations to avoid missing 50 | important server updates. The Client example below demonstrates correct response 51 | handling. 52 | 53 | Concurrency 54 | 55 | The Client and its Command objects cannot be used concurrently from multiple 56 | goroutines. It is safe to pass Response objects to other goroutines for 57 | processing, but the Client assumes "single-threaded" model of operation, so all 58 | method calls for the same connection must be serialized with sync.Mutex or some 59 | other synchronization mechanism. Likewise, it is not safe to access Client.Data 60 | and Command.Data in parallel with a call that can append new responses to these 61 | fields. 62 | 63 | Asynchronous Commands 64 | 65 | Unless a command is marked as being "synchronous", which is usually those 66 | commands that change the connection state, the associated method returns as soon 67 | as the command is sent to the server, without waiting for completion. This 68 | allows the client to issue multiple concurrent commands, and then process the 69 | responses and command completions as they arrive. 70 | 71 | A call to Command.Result on a command that is "in progress" will block until 72 | that command is finished. There is also a convenience function that turns any 73 | asynchronous command into a synchronous one: 74 | 75 | cmd, err := imap.Wait(c.Fetch(...)) 76 | 77 | If err is nil when the call returns, the command was completed with the OK 78 | status and all data responses (if any) are queued in cmd.Data. 79 | 80 | Logging Out 81 | 82 | The Client launches a goroutine to support receive operations with timeouts. The 83 | user must call Client.Logout to close the connection and stop the goroutine. The 84 | only time it is unnecessary to call Client.Logout is when the server closes the 85 | connection first and Client.Recv returns io.EOF error. 86 | 87 | RFCs 88 | 89 | The following RFCs are implemented by this package: 90 | 91 | http://tools.ietf.org/html/rfc2087 -- IMAP4 QUOTA extension 92 | http://tools.ietf.org/html/rfc2088 -- IMAP4 non-synchronizing literals 93 | http://tools.ietf.org/html/rfc2177 -- IMAP4 IDLE command 94 | http://tools.ietf.org/html/rfc2971 -- IMAP4 ID extension 95 | http://tools.ietf.org/html/rfc3501 -- INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 96 | http://tools.ietf.org/html/rfc3516 -- IMAP4 Binary Content Extension 97 | http://tools.ietf.org/html/rfc3691 -- Internet Message Access Protocol (IMAP) UNSELECT command 98 | http://tools.ietf.org/html/rfc4315 -- Internet Message Access Protocol (IMAP) - UIDPLUS extension 99 | http://tools.ietf.org/html/rfc4616 -- The PLAIN Simple Authentication and Security Layer (SASL) Mechanism 100 | http://tools.ietf.org/html/rfc4959 -- IMAP Extension for Simple Authentication and Security Layer (SASL) Initial Client Response 101 | http://tools.ietf.org/html/rfc4978 -- The IMAP COMPRESS Extension 102 | http://tools.ietf.org/html/rfc5161 -- The IMAP ENABLE Extension 103 | http://tools.ietf.org/html/rfc5738 -- IMAP Support for UTF-8 104 | 105 | The following RFCs are either informational, not fully implemented, or place no 106 | implementation requirements on the package, but may be relevant to other parts 107 | of a client application: 108 | 109 | http://tools.ietf.org/html/rfc2595 -- Using TLS with IMAP, POP3 and ACAP 110 | http://tools.ietf.org/html/rfc2683 -- IMAP4 Implementation Recommendations 111 | http://tools.ietf.org/html/rfc4466 -- Collected Extensions to IMAP4 ABNF 112 | http://tools.ietf.org/html/rfc4469 -- Internet Message Access Protocol (IMAP) CATENATE Extension 113 | http://tools.ietf.org/html/rfc4549 -- Synchronization Operations for Disconnected IMAP4 Clients 114 | http://tools.ietf.org/html/rfc5530 -- IMAP Response Codes 115 | */ 116 | package imap 117 | -------------------------------------------------------------------------------- /imap/enum.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import "strconv" 8 | 9 | // ConnState represents client connection states. See RFC 3501 page 15 for a 10 | // state diagram. 11 | type ConnState uint8 12 | 13 | // Client connection states. 14 | const ( 15 | unknown = ConnState(1 << iota) // Pre-greeting internal state 16 | Login // Not authenticated 17 | Auth // Authenticated 18 | Selected // Mailbox selected 19 | Logout // Connection closing 20 | Closed = ConnState(0) // Connection closed 21 | ) 22 | 23 | var connStates = []enumName{ 24 | {uint32(unknown), "unknown"}, 25 | {uint32(Login), "Login"}, 26 | {uint32(Auth), "Auth"}, 27 | {uint32(Selected), "Selected"}, 28 | {uint32(Logout), "Logout"}, 29 | {uint32(Closed), "Closed"}, 30 | } 31 | 32 | func (v ConnState) String() string { return enumString(uint32(v), connStates, false) } 33 | func (v ConnState) GoString() string { return enumString(uint32(v), connStates, true) } 34 | 35 | // RespType indicates the type of information contained in the response. 36 | type RespType uint8 37 | 38 | // Server response types. 39 | const ( 40 | Status = RespType(1 << iota) // Untagged status 41 | Data // Untagged data 42 | Continue // Continuation request 43 | Done // Tagged command completion 44 | ) 45 | 46 | var respTypes = []enumName{ 47 | {uint32(Status), "Status"}, 48 | {uint32(Data), "Data"}, 49 | {uint32(Continue), "Continue"}, 50 | {uint32(Done), "Done"}, 51 | } 52 | 53 | func (v RespType) String() string { return enumString(uint32(v), respTypes, false) } 54 | func (v RespType) GoString() string { return enumString(uint32(v), respTypes, true) } 55 | 56 | // RespStatus is the code sent in status messages to indicate success, failure, 57 | // or changes in the connection state. 58 | type RespStatus uint8 59 | 60 | // Status conditions used by Status and Done response types. 61 | const ( 62 | OK = RespStatus(1 << iota) // Success 63 | NO // Operational error 64 | BAD // Protocol-level error 65 | PREAUTH // Greeting status indicating Auth state (untagged-only) 66 | BYE // Connection closing (untagged-only) 67 | ) 68 | 69 | var respStatuses = []enumName{ 70 | {uint32(OK), "OK"}, 71 | {uint32(NO), "NO"}, 72 | {uint32(BAD), "BAD"}, 73 | {uint32(PREAUTH), "PREAUTH"}, 74 | {uint32(BYE), "BYE"}, 75 | } 76 | 77 | func (v RespStatus) String() string { return enumString(uint32(v), respStatuses, false) } 78 | func (v RespStatus) GoString() string { return enumString(uint32(v), respStatuses, true) } 79 | 80 | // FieldType describes the data type of a single response field. 81 | type FieldType uint8 82 | 83 | // Valid field data types. 84 | const ( 85 | Atom = FieldType(1 << iota) // String consisting of non-special ASCII characters 86 | Number // Unsigned 32-bit integer 87 | QuotedString // String enclosed in double quotes 88 | LiteralString // String or binary data 89 | List // Parenthesized list 90 | Bytes // Decoded Base64 data 91 | NIL // Case-insensitive atom string "NIL" 92 | ) 93 | 94 | var fieldTypes = []enumName{ 95 | {uint32(Atom), "Atom"}, 96 | {uint32(Number), "Number"}, 97 | {uint32(QuotedString), "QuotedString"}, 98 | {uint32(LiteralString), "LiteralString"}, 99 | {uint32(List), "List"}, 100 | {uint32(Bytes), "Bytes"}, 101 | {uint32(NIL), "NIL"}, 102 | } 103 | 104 | func (v FieldType) String() string { return enumString(uint32(v), fieldTypes, false) } 105 | func (v FieldType) GoString() string { return enumString(uint32(v), fieldTypes, true) } 106 | 107 | // LogMask represents the categories of debug messages that can be logged by the 108 | // Client. 109 | type LogMask uint8 110 | 111 | // Debug message categories. 112 | const ( 113 | LogConn = LogMask(1 << iota) // Connection events 114 | LogState // State changes 115 | LogCmd // Command execution 116 | LogRaw // Raw data stream excluding literals 117 | LogGo // Goroutine execution 118 | LogAll = LogMask(1< 0 { 146 | s += "+" 147 | } 148 | if goSyntax { 149 | s += "imap." 150 | } 151 | s += n.s 152 | if v &= ^n.v; v == 0 { 153 | return s 154 | } 155 | } 156 | } 157 | if len(s) > 0 { 158 | s += "+" 159 | } 160 | return s + "0x" + strconv.FormatUint(uint64(v), 16) 161 | } 162 | -------------------------------------------------------------------------------- /imap/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap_test 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/mail" 11 | "time" 12 | 13 | "github.com/mxk/go-imap/imap" 14 | ) 15 | 16 | func ExampleClient() { 17 | // 18 | // Note: most of error handling code is omitted for brevity 19 | // 20 | var ( 21 | c *imap.Client 22 | cmd *imap.Command 23 | rsp *imap.Response 24 | ) 25 | 26 | // Connect to the server 27 | c, _ = imap.Dial("imap.example.com") 28 | 29 | // Remember to log out and close the connection when finished 30 | defer c.Logout(30 * time.Second) 31 | 32 | // Print server greeting (first response in the unilateral server data queue) 33 | fmt.Println("Server says hello:", c.Data[0].Info) 34 | c.Data = nil 35 | 36 | // Enable encryption, if supported by the server 37 | if c.Caps["STARTTLS"] { 38 | c.StartTLS(nil) 39 | } 40 | 41 | // Authenticate 42 | if c.State() == imap.Login { 43 | c.Login("user@example.com", "mysupersecretpassword") 44 | } 45 | 46 | // List all top-level mailboxes, wait for the command to finish 47 | cmd, _ = imap.Wait(c.List("", "%")) 48 | 49 | // Print mailbox information 50 | fmt.Println("\nTop-level mailboxes:") 51 | for _, rsp = range cmd.Data { 52 | fmt.Println("|--", rsp.MailboxInfo()) 53 | } 54 | 55 | // Check for new unilateral server data responses 56 | for _, rsp = range c.Data { 57 | fmt.Println("Server data:", rsp) 58 | } 59 | c.Data = nil 60 | 61 | // Open a mailbox (synchronous command - no need for imap.Wait) 62 | c.Select("INBOX", true) 63 | fmt.Print("\nMailbox status:\n", c.Mailbox) 64 | 65 | // Fetch the headers of the 10 most recent messages 66 | set, _ := imap.NewSeqSet("") 67 | if c.Mailbox.Messages >= 10 { 68 | set.AddRange(c.Mailbox.Messages-9, c.Mailbox.Messages) 69 | } else { 70 | set.Add("1:*") 71 | } 72 | cmd, _ = c.Fetch(set, "RFC822.HEADER") 73 | 74 | // Process responses while the command is running 75 | fmt.Println("\nMost recent messages:") 76 | for cmd.InProgress() { 77 | // Wait for the next response (no timeout) 78 | c.Recv(-1) 79 | 80 | // Process command data 81 | for _, rsp = range cmd.Data { 82 | header := imap.AsBytes(rsp.MessageInfo().Attrs["RFC822.HEADER"]) 83 | if msg, _ := mail.ReadMessage(bytes.NewReader(header)); msg != nil { 84 | fmt.Println("|--", msg.Header.Get("Subject")) 85 | } 86 | } 87 | cmd.Data = nil 88 | 89 | // Process unilateral server data 90 | for _, rsp = range c.Data { 91 | fmt.Println("Server data:", rsp) 92 | } 93 | c.Data = nil 94 | } 95 | 96 | // Check command completion status 97 | if rsp, err := cmd.Result(imap.OK); err != nil { 98 | if err == imap.ErrAborted { 99 | fmt.Println("Fetch command aborted") 100 | } else { 101 | fmt.Println("Fetch error:", rsp.Info) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /imap/field.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "sort" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // Date-time format used by INTERNALDATE. 16 | const DATETIME = `"_2-Jan-2006 15:04:05 -0700"` 17 | 18 | // Field represents a single data item in a command or response. Fields are 19 | // separated from one another by a single space. Field slices represent 20 | // parenthesized lists. 21 | type Field interface{} 22 | 23 | // TypeOf returns the field data type. Valid types are Atom, Number, 24 | // QuotedString, LiteralString, List, Bytes, and NIL. Zero is returned for 25 | // unknown data types. 26 | func TypeOf(f Field) FieldType { 27 | switch f.(type) { 28 | case string: 29 | if Quoted(f) { 30 | return QuotedString 31 | } else if len(f.(string)) > 0 { 32 | return Atom 33 | } 34 | case uint32: 35 | return Number 36 | case []Field: 37 | return List 38 | case []byte: 39 | return Bytes 40 | case Literal: 41 | return LiteralString 42 | case nil: 43 | return NIL 44 | } 45 | return 0 46 | } 47 | 48 | // AsAtom returns the value of an atom field. An empty string is returned if 49 | // TypeOf(f) != Atom. 50 | func AsAtom(f Field) string { 51 | if v, ok := f.(string); ok && !Quoted(f) { 52 | return v 53 | } 54 | return "" 55 | } 56 | 57 | // AsNumber returns the value of a numeric field. Zero is returned if TypeOf(f) 58 | // != Number. 59 | func AsNumber(f Field) uint32 { 60 | v, _ := f.(uint32) 61 | return v 62 | } 63 | 64 | // AsString returns the value of an astring (string or atom) field. Quoted 65 | // strings are decoded to their original representation. An empty string is 66 | // returned if TypeOf(f)&(Atom|QuotedString|LiteralString) == 0 or the string is 67 | // invalid. 68 | func AsString(f Field) string { 69 | if v, ok := f.(string); ok { 70 | if Quoted(f) { 71 | v, _ = Unquote(v) 72 | } 73 | return v 74 | } else if _, ok = f.(Literal); ok { 75 | return string(AsBytes(f)) 76 | } 77 | return "" 78 | } 79 | 80 | // AsBytes returns the value of a data field. Nil is returned if 81 | // TypeOf(f)&(QuotedString|LiteralString|Bytes) == 0. 82 | func AsBytes(f Field) []byte { 83 | switch v := f.(type) { 84 | case []byte: 85 | return v 86 | case string: 87 | if Quoted(f) { 88 | b, _ := unquote([]byte(v)) 89 | return b 90 | } 91 | case *literal: 92 | return v.data 93 | case Literal: 94 | if n := v.Info().Len; n > 0 { 95 | b := bytes.NewBuffer(make([]byte, 0, n)) 96 | if _, err := v.WriteTo(b); err == nil && uint32(b.Len()) == n { 97 | return b.Bytes() 98 | } 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | // AsList returns the value of a parenthesized list. Nil is returned if 105 | // TypeOf(f) != List. 106 | func AsList(f Field) []Field { 107 | v, _ := f.([]Field) 108 | return v 109 | } 110 | 111 | // AsDateTime returns the value of a date-time quoted string field (e.g. 112 | // INTERNALDATE). The zero value of time.Time is returned if f does not contain 113 | // a valid date-time string. 114 | func AsDateTime(f Field) time.Time { 115 | s, _ := f.(string) 116 | if v, err := time.Parse(DATETIME, s); err == nil { 117 | return v 118 | } 119 | return time.Time{} 120 | } 121 | 122 | // AsMailbox returns the value of a mailbox name field. All valid atoms and 123 | // strings encoded as quoted UTF-8 or modified UTF-7 are decoded appropriately. 124 | // The special case-insensitive name "INBOX" is always converted to upper case. 125 | func AsMailbox(f Field) string { 126 | v := AsString(f) 127 | if len(v) == 5 && toUpper(v) == "INBOX" { 128 | return "INBOX" 129 | } else if !QuotedUTF8(f) { 130 | if s, err := UTF7Decode(v); err == nil { 131 | return s 132 | } 133 | } 134 | return v 135 | } 136 | 137 | // FieldMap represents key-value pairs of data items, such as those returned in 138 | // a FETCH response. Key names are atoms converted to upper case. 139 | type FieldMap map[string]Field 140 | 141 | // AsFieldMap returns a map of key-value pairs extracted from a parenthesized 142 | // list. Nil is returned if TypeOf(f) != List, the number of fields in the list 143 | // is not even, or one of the keys is not an Atom. 144 | func AsFieldMap(f Field) FieldMap { 145 | list, ok := f.([]Field) 146 | n := len(list) 147 | if !ok || n&1 == 1 { // n must be even; initialize the map for n == 0 148 | return nil 149 | } 150 | v := make(FieldMap, n/2) 151 | for i := 0; i < n; i += 2 { 152 | if k := toUpper(AsAtom(list[i])); k != "" { 153 | v[k] = list[i+1] 154 | } else { 155 | return nil 156 | } 157 | } 158 | return v 159 | } 160 | 161 | func (fm FieldMap) String() string { 162 | if len(fm) == 0 { 163 | return "()" 164 | } 165 | v, i := make([]string, len(fm)), 0 166 | for k := range fm { 167 | v[i] = k 168 | i++ 169 | } 170 | sort.Strings(v) 171 | for i, k := range v { 172 | v[i] = fmt.Sprint(k, ":", fm[k]) 173 | } 174 | return "(" + strings.Join(v, " ") + ")" 175 | } 176 | 177 | // FlagSet represents the flags enabled for a single mailbox or message. The map 178 | // values are always set to true; a flag must be deleted from the map to 179 | // indicate that it is not enabled. 180 | type FlagSet map[string]bool 181 | 182 | // NewFlagSet returns a new flag set with the specified flags enabled. 183 | func NewFlagSet(flags ...string) FlagSet { 184 | fs := make(FlagSet, len(flags)) 185 | for _, v := range flags { 186 | fs[v] = true 187 | } 188 | return fs 189 | } 190 | 191 | // AsFlags returns a set of flags extracted from a parenthesized list. The 192 | // function does not check every atom for the leading backslash, because it is 193 | // not permitted in user-defined flags (keywords). Nil is returned if TypeOf(f) 194 | // != List or one of the fields is not an atom. 195 | func AsFlagSet(f Field) FlagSet { 196 | list, ok := f.([]Field) 197 | if !ok { 198 | return nil 199 | } 200 | v := make(FlagSet, len(list)) 201 | for _, f := range list { 202 | if s := AsAtom(f); s != "" { 203 | v[s] = true 204 | } else { 205 | return nil 206 | } 207 | } 208 | return v 209 | } 210 | 211 | // Replace removes all existing flags from the set and inserts new ones. 212 | func (fs FlagSet) Replace(f Field) { 213 | if list, ok := f.([]Field); ok { 214 | for v := range fs { 215 | delete(fs, v) 216 | } 217 | for _, f := range list { 218 | if v := AsAtom(f); v != "" { 219 | fs[v] = true 220 | } 221 | } 222 | } 223 | } 224 | 225 | func (fs FlagSet) String() string { 226 | if len(fs) == 0 { 227 | return "()" 228 | } 229 | v, i := make([]string, len(fs)), 0 230 | for k := range fs { 231 | v[i] = k 232 | i++ 233 | } 234 | sort.Strings(v) 235 | return "(" + strings.Join(v, " ") + ")" 236 | } 237 | 238 | // intValue converts any signed integer value to int64. It panics if f is not a 239 | // signed integer. 240 | func intValue(f Field) int64 { 241 | switch v := f.(type) { 242 | case int: 243 | return int64(v) 244 | case int8: 245 | return int64(v) 246 | case int16: 247 | return int64(v) 248 | case int32: 249 | return int64(v) 250 | case int64: 251 | return int64(v) 252 | } 253 | panic("imap: not an int") 254 | } 255 | 256 | // uintValue converts any unsigned integer value to uint64. It panics if f is 257 | // not an unsigned integer. 258 | func uintValue(f Field) uint64 { 259 | switch v := f.(type) { 260 | case uint: 261 | return uint64(v) 262 | case uint8: 263 | return uint64(v) 264 | case uint16: 265 | return uint64(v) 266 | case uint32: 267 | return uint64(v) 268 | case uint64: 269 | return uint64(v) 270 | } 271 | panic("imap: not a uint") 272 | } 273 | -------------------------------------------------------------------------------- /imap/field_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type xlit struct{ Literal } 16 | 17 | func fname(v reflect.Value) (name string) { 18 | name = runtime.FuncForPC(v.Pointer()).Name() 19 | if i := strings.LastIndex(name, "."); i != -1 { 20 | name = name[i+1:] 21 | } 22 | return name 23 | } 24 | 25 | func TestField(t *testing.T) { 26 | tests := []struct { 27 | call interface{} 28 | in Field 29 | out interface{} 30 | }{ 31 | {TypeOf, nil, NIL}, 32 | {TypeOf, ``, FieldType(0)}, 33 | {TypeOf, 42, FieldType(0)}, 34 | 35 | {TypeOf, `x`, Atom}, 36 | {TypeOf, `FETCH`, Atom}, 37 | {TypeOf, `\Seen`, Atom}, 38 | 39 | {TypeOf, uint32(0), Number}, 40 | {TypeOf, uint32(1), Number}, 41 | {TypeOf, ^uint32(0), Number}, 42 | 43 | {TypeOf, `""`, QuotedString}, 44 | {TypeOf, `*""`, QuotedString}, 45 | {TypeOf, `"x"`, QuotedString}, 46 | {TypeOf, `*"x"`, QuotedString}, 47 | {TypeOf, `"42"`, QuotedString}, 48 | {TypeOf, `"\"`, QuotedString}, 49 | {TypeOf, `"\x"`, QuotedString}, 50 | 51 | {TypeOf, (*literal)(nil), LiteralString}, 52 | {TypeOf, lit(``), LiteralString}, 53 | {TypeOf, lit(`x`), LiteralString}, 54 | {TypeOf, lit8(``), LiteralString}, 55 | {TypeOf, lit8(`x`), LiteralString}, 56 | {TypeOf, xlit{lit(``)}, LiteralString}, 57 | {TypeOf, xlit{lit(`x`)}, LiteralString}, 58 | {TypeOf, xlit{lit8(``)}, LiteralString}, 59 | {TypeOf, xlit{lit8(`x`)}, LiteralString}, 60 | 61 | {TypeOf, []Field(nil), List}, 62 | {TypeOf, []Field{``}, List}, 63 | {TypeOf, []Field{`x`}, List}, 64 | {TypeOf, []Field{`\Seen`, `\Flagged`}, List}, 65 | 66 | {TypeOf, []byte(nil), Bytes}, 67 | {TypeOf, []byte(``), Bytes}, 68 | {TypeOf, []byte(`x`), Bytes}, 69 | 70 | {AsAtom, nil, ``}, 71 | {AsAtom, ``, ``}, 72 | {AsAtom, 42, ``}, 73 | {AsAtom, `""`, ``}, 74 | {AsAtom, `"x"`, ``}, 75 | {AsAtom, `*"x"`, ``}, 76 | {AsAtom, lit(`x`), ``}, 77 | {AsAtom, []byte(`x`), ``}, 78 | {AsAtom, `x`, `x`}, 79 | {AsAtom, `FETCH`, `FETCH`}, 80 | {AsAtom, `\seen`, `\seen`}, 81 | 82 | {AsNumber, nil, uint32(0)}, 83 | {AsNumber, 0, uint32(0)}, 84 | {AsNumber, 1, uint32(0)}, 85 | {AsNumber, ``, uint32(0)}, 86 | {AsNumber, `1`, uint32(0)}, 87 | {AsNumber, []byte{1}, uint32(0)}, 88 | {AsNumber, uint32(0), uint32(0)}, 89 | {AsNumber, uint32(1), uint32(1)}, 90 | {AsNumber, ^uint32(0), ^uint32(0)}, 91 | 92 | {AsString, nil, ``}, 93 | {AsString, ``, ``}, 94 | {AsString, `"\"`, ``}, 95 | {AsString, `"\x"`, ``}, 96 | {AsString, []byte(nil), ``}, 97 | {AsString, []byte(`x`), ``}, 98 | {AsString, []byte(`"x"`), ``}, 99 | {AsString, `x`, `x`}, 100 | {AsString, `""`, ``}, 101 | {AsString, `*""`, ``}, 102 | {AsString, `"x"`, `x`}, 103 | {AsString, `"\""`, `"`}, 104 | {AsString, `*"x"`, `x`}, 105 | {AsString, lit(``), ``}, 106 | {AsString, lit(`x`), `x`}, 107 | {AsString, lit(`"`), `"`}, 108 | {AsString, lit8(``), ``}, 109 | {AsString, lit8(`x`), `x`}, 110 | {AsString, lit8(`x\"`), `x\"`}, 111 | {AsString, xlit{lit(``)}, ``}, 112 | {AsString, xlit{lit("\x00\r\n")}, "\x00\r\n"}, 113 | 114 | {AsBytes, nil, []byte(nil)}, 115 | {AsBytes, ``, []byte(nil)}, 116 | {AsBytes, `x`, []byte(nil)}, 117 | {AsBytes, `"\"`, []byte(nil)}, 118 | {AsBytes, `"\x"`, []byte(nil)}, 119 | {AsBytes, []byte(nil), []byte(nil)}, 120 | {AsBytes, []byte(`x`), []byte(`x`)}, 121 | {AsBytes, []byte(`"x"`), []byte(`"x"`)}, 122 | {AsBytes, `""`, []byte(nil)}, 123 | {AsBytes, `*""`, []byte(nil)}, 124 | {AsBytes, `"x"`, []byte(`x`)}, 125 | {AsBytes, `"\""`, []byte(`"`)}, 126 | {AsBytes, `*"x"`, []byte(`x`)}, 127 | {AsBytes, lit(``), []byte(nil)}, 128 | {AsBytes, lit(`x`), []byte(`x`)}, 129 | {AsBytes, lit8(`x`), []byte(`x`)}, 130 | {AsBytes, lit8(`\x00\r\n`), []byte(`\x00\r\n`)}, 131 | {AsBytes, xlit{lit(``)}, []byte(nil)}, 132 | {AsBytes, xlit{lit(`x`)}, []byte(`x`)}, 133 | {AsBytes, xlit{lit8(`x`)}, []byte(`x`)}, 134 | {AsBytes, xlit{lit8(`\x00\r\n`)}, []byte(`\x00\r\n`)}, 135 | 136 | {AsList, nil, []Field(nil)}, 137 | {AsList, ``, []Field(nil)}, 138 | {AsList, []byte(`42`), []Field(nil)}, 139 | {AsList, []Field(nil), []Field(nil)}, 140 | {AsList, []Field{}, []Field{}}, 141 | {AsList, []Field{`x`}, []Field{"x"}}, 142 | {AsList, []Field{`\Seen`, `\Flagged`}, []Field{`\Seen`, `\Flagged`}}, 143 | 144 | {AsDateTime, nil, time.Time{}}, 145 | {AsDateTime, time.Now(), time.Time{}}, 146 | {AsDateTime, ``, time.Time{}}, 147 | {AsDateTime, `""`, time.Time{}}, 148 | {AsDateTime, `"17-Jul-1996"`, time.Time{}}, 149 | {AsDateTime, `"02:44:25"`, time.Time{}}, 150 | {AsDateTime, `"17-Jul-1996 02:44:25"`, time.Time{}}, 151 | {AsDateTime, `*"17-Jul-1996 02:44:25 -0700"`, time.Time{}}, 152 | {AsDateTime, `"17-Jul-1996 02:44:25 -0700"`, time.Date(1996, time.July, 17, 2, 44, 25, 0, MST)}, 153 | {AsDateTime, `"07-Jul-1996 02:44:25 -0700"`, time.Date(1996, time.July, 7, 2, 44, 25, 0, MST)}, 154 | {AsDateTime, `" 7-Jul-1996 2:44:25 -0700"`, time.Date(1996, time.July, 7, 2, 44, 25, 0, MST)}, 155 | {AsDateTime, `"7-Jul-1996 2:44:25 -0700"`, time.Date(1996, time.July, 7, 2, 44, 25, 0, MST)}, 156 | {AsDateTime, `"7-Jul-1996 00:00:00 -0700"`, time.Date(1996, time.July, 7, 0, 0, 0, 0, MST)}, 157 | {AsDateTime, `"7-Jul-1996 0:10:01 -0700"`, time.Date(1996, time.July, 7, 0, 10, 1, 0, MST)}, 158 | 159 | {AsMailbox, nil, ``}, 160 | {AsMailbox, ``, ``}, 161 | {AsMailbox, `x`, `x`}, 162 | {AsMailbox, `\"`, `\"`}, 163 | {AsMailbox, `""`, ``}, 164 | {AsMailbox, `"x"`, `x`}, 165 | {AsMailbox, `"\""`, `"`}, 166 | {AsMailbox, `&`, `&`}, 167 | {AsMailbox, `&-`, `&`}, 168 | {AsMailbox, `&x`, `&x`}, 169 | {AsMailbox, `"&"`, `&`}, 170 | {AsMailbox, `"&-"`, `&`}, 171 | {AsMailbox, `"&x"`, `&x`}, 172 | {AsMailbox, `*"&-"`, `&-`}, 173 | {AsMailbox, lit(`&`), `&`}, 174 | {AsMailbox, lit(`&-`), `&`}, 175 | {AsMailbox, lit(`&x`), `&x`}, 176 | {AsMailbox, lit(`"&-"`), `"&"`}, 177 | {AsMailbox, lit(`*"&-"`), `*"&"`}, 178 | {AsMailbox, lit(`&Jjo!`), `&Jjo!`}, 179 | {AsMailbox, `inbox`, `INBOX`}, 180 | {AsMailbox, `"iNbOx"`, `INBOX`}, 181 | {AsMailbox, lit(`InBoX`), `INBOX`}, 182 | {AsMailbox, lit(`"Inbox"`), `"Inbox"`}, 183 | 184 | {AsFieldMap, nil, FieldMap(nil)}, 185 | {AsFieldMap, uint32(1), FieldMap(nil)}, 186 | {AsFieldMap, []byte(`xy`), FieldMap(nil)}, 187 | {AsFieldMap, FieldMap{`x`: `y`}, FieldMap(nil)}, 188 | {AsFieldMap, []Field{`x`}, FieldMap(nil)}, 189 | {AsFieldMap, []Field{``, `x`}, FieldMap(nil)}, 190 | {AsFieldMap, []Field{`x`, `y`, `z`}, FieldMap(nil)}, 191 | {AsFieldMap, []Field{`"X"`, uint32(42)}, FieldMap(nil)}, 192 | {AsFieldMap, []Field(nil), FieldMap{}}, 193 | {AsFieldMap, []Field{}, FieldMap{}}, 194 | {AsFieldMap, []Field{`X`, uint32(42)}, FieldMap{`X`: uint32(42)}}, 195 | {AsFieldMap, []Field{`x`, `y`, `z`, nil}, FieldMap{`X`: `y`, `Z`: nil}}, 196 | {AsFieldMap, []Field{`x`, `"y"`, `z`, []Field(nil)}, FieldMap{`X`: `"y"`, `Z`: []Field(nil)}}, 197 | 198 | {AsFlagSet, nil, FlagSet(nil)}, 199 | {AsFlagSet, uint32(1), FlagSet(nil)}, 200 | {AsFlagSet, []byte(`\Seen`), FlagSet(nil)}, 201 | {AsFlagSet, NewFlagSet(`\Seen`), FlagSet(nil)}, 202 | {AsFlagSet, []Field{``}, FlagSet(nil)}, 203 | {AsFlagSet, []Field{`"\Seen"`}, FlagSet(nil)}, 204 | {AsFlagSet, []Field(nil), FlagSet{}}, 205 | {AsFlagSet, []Field{}, FlagSet{}}, 206 | {AsFlagSet, []Field{`x`}, FlagSet{`x`: true}}, 207 | {AsFlagSet, []Field{`x`}, NewFlagSet(`x`)}, 208 | {AsFlagSet, []Field{`x`, `y`}, NewFlagSet(`x`, `y`)}, 209 | {AsFlagSet, []Field{`\Seen`, `\deleted`}, NewFlagSet(`\Seen`, `\deleted`)}, 210 | } 211 | for _, test := range tests { 212 | call := reflect.ValueOf(test.call) 213 | args := []reflect.Value{reflect.ValueOf(&test.in).Elem()} 214 | out := call.Call(args)[0].Interface() 215 | if !reflect.DeepEqual(out, test.out) { 216 | t.Errorf("%s(%#v) expected %v; got %v", fname(call), test.in, test.out, out) 217 | } 218 | } 219 | in := lit("x") 220 | out := AsBytes(in) 221 | if reflect.ValueOf(out).Pointer() != reflect.ValueOf(in.(*literal).data).Pointer() { 222 | t.Errorf("AsBytes took the slow path for *literal") 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /imap/imap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "crypto/tls" 9 | "io" 10 | "net" 11 | "time" 12 | ) 13 | 14 | // Timeout values for the Dial functions. 15 | const ( 16 | netTimeout = 30 * time.Second // Time to establish a TCP connection 17 | clientTimeout = 60 * time.Second // Time to receive greeting and capabilities 18 | ) 19 | 20 | // Dial returns a new Client connected to an IMAP server at addr. 21 | func Dial(addr string) (c *Client, err error) { 22 | addr = defaultPort(addr, "143") 23 | conn, err := net.DialTimeout("tcp", addr, netTimeout) 24 | if err == nil { 25 | host, _, _ := net.SplitHostPort(addr) 26 | if c, err = NewClient(conn, host, clientTimeout); err != nil { 27 | conn.Close() 28 | } 29 | } 30 | return 31 | } 32 | 33 | // DialTLS returns a new Client connected to an IMAP server at addr using the 34 | // specified config for encryption. 35 | func DialTLS(addr string, config *tls.Config) (c *Client, err error) { 36 | addr = defaultPort(addr, "993") 37 | conn, err := net.DialTimeout("tcp", addr, netTimeout) 38 | if err == nil { 39 | host, _, _ := net.SplitHostPort(addr) 40 | tlsConn := tls.Client(conn, setServerName(config, host)) 41 | if c, err = NewClient(tlsConn, host, clientTimeout); err != nil { 42 | conn.Close() 43 | } 44 | } 45 | return 46 | } 47 | 48 | // Wait is a convenience function for transforming asynchronous commands into 49 | // synchronous ones. The error is nil if and only if the command is completed 50 | // with OK status condition. Usage example: 51 | // 52 | // cmd, err := imap.Wait(c.Fetch(...)) 53 | func Wait(cmd *Command, err error) (*Command, error) { 54 | if err == nil { 55 | _, err = cmd.Result(OK) 56 | } 57 | return cmd, err 58 | } 59 | 60 | // Capability requests a listing of capabilities supported by the server. The 61 | // client automatically requests capabilities when the connection is first 62 | // established, after a successful STARTTLS command, and after user 63 | // authentication, making it unnecessary to call this method directly in most 64 | // cases. The current capabilities are available in c.Caps. 65 | // 66 | // This command is synchronous. 67 | func (c *Client) Capability() (cmd *Command, err error) { 68 | return Wait(c.Send("CAPABILITY")) 69 | } 70 | 71 | // Noop does nothing, but it allows the server to send status updates, which are 72 | // delivered to the unilateral server data queue (c.Data). It can also be used 73 | // to reset any inactivity autologout timer on the server. 74 | func (c *Client) Noop() (cmd *Command, err error) { 75 | return c.Send("NOOP") 76 | } 77 | 78 | // Logout informs the server that the client is done with the connection. This 79 | // method must be called to close the connection and free all client resources. 80 | // 81 | // A negative timeout allows the client to wait indefinitely for the normal 82 | // logout sequence to complete. A timeout of 0 causes the connection to be 83 | // closed immediately without actually sending the LOGOUT command. A positive 84 | // timeout behaves as expected, returning ErrTimeout if the normal logout 85 | // sequence is not completed in the allocated time. The connection is always 86 | // closed when this method returns. 87 | // 88 | // This command is synchronous. 89 | func (c *Client) Logout(timeout time.Duration) (cmd *Command, err error) { 90 | if c.state == Closed { 91 | return nil, ErrNotAllowed 92 | } 93 | defer c.setState(Closed) 94 | defer c.close("logout error") 95 | 96 | c.setState(Logout) 97 | if timeout == 0 { 98 | err = c.close("immediate logout") 99 | } else { 100 | if timeout > 0 { 101 | c.t.conn.SetDeadline(time.Now().Add(timeout)) 102 | } 103 | cmd, err = Wait(c.Send("LOGOUT")) 104 | } 105 | for err == nil { 106 | if err = c.Recv(block); err == io.EOF { 107 | return cmd, nil 108 | } 109 | } 110 | if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 111 | err = ErrTimeout 112 | } 113 | return 114 | } 115 | 116 | // StartTLS enables session privacy protection and integrity checking. The 117 | // server must advertise STARTTLS capability for this command to be available. 118 | // The client automatically requests new capabilities if the TLS handshake is 119 | // successful. 120 | // 121 | // This command is synchronous. 122 | func (c *Client) StartTLS(config *tls.Config) (cmd *Command, err error) { 123 | if !c.Caps["STARTTLS"] { 124 | return nil, NotAvailableError("STARTTLS") 125 | } else if c.t.Encrypted() { 126 | return nil, ErrEncryptionActive 127 | } 128 | if cmd, err = Wait(c.Send("STARTTLS")); err == nil { 129 | if c.rch != nil { 130 | // Should never happen 131 | panic("imap: receiver is active, cannot perform TLS handshake") 132 | } 133 | if err = c.t.EnableTLS(setServerName(config, c.host)); err == nil { 134 | _, err = c.Capability() 135 | } 136 | } 137 | return 138 | } 139 | 140 | // Auth performs SASL challenge-response authentication. The client 141 | // automatically requests new capabilities if authentication is successful. 142 | // 143 | // This command is synchronous. 144 | func (c *Client) Auth(a SASL) (cmd *Command, err error) { 145 | info := ServerInfo{c.host, c.t.Encrypted(), c.getCaps("AUTH=")} 146 | mech, cr, err := a.Start(&info) 147 | if err != nil { 148 | return 149 | } else if name := "AUTH=" + mech; !c.Caps[name] { 150 | return nil, NotAvailableError(name) 151 | } 152 | args := []Field{mech, nil}[:1] 153 | 154 | // Initial response is sent with the command if the server supports SASL-IR 155 | if cr = b64enc(cr); cr != nil && c.Caps["SASL-IR"] { 156 | if len(cr) > 0 { 157 | args = append(args, cr) 158 | } else { 159 | args = append(args, "=") 160 | } 161 | cr = nil 162 | } 163 | cmd, err = c.Send("AUTHENTICATE", args...) 164 | 165 | // Challenge-response loop 166 | var rsp *Response 167 | var abort error 168 | for err == nil && cmd.InProgress() { 169 | rsp, err = c.checkContinue(cmd, true) 170 | if err == nil && rsp.Type == Continue { 171 | if cr == nil { 172 | if cr, abort = a.Next(rsp.Challenge()); abort == nil { 173 | cr = b64enc(cr) 174 | } else if err = c.t.WriteLine([]byte("*")); err == nil { 175 | err = c.t.Flush() 176 | break 177 | } 178 | } 179 | err, cr = c.t.WriteLine(cr), nil 180 | } 181 | } 182 | 183 | // Wait for command completion 184 | if err == nil { 185 | if rsp, err = cmd.Result(OK); err == nil { 186 | c.setState(Auth) 187 | if rsp.Label != "CAPABILITY" { 188 | _, err = c.Capability() 189 | } 190 | } else if abort != nil && rsp != nil && rsp.Status == BAD { 191 | err = abort 192 | } 193 | } 194 | return 195 | } 196 | 197 | // Login performs plaintext username/password authentication. This command is 198 | // disabled when the server advertises LOGINDISABLED capability. The client 199 | // automatically requests new capabilities if authentication is successful. 200 | // 201 | // This command is synchronous. 202 | func (c *Client) Login(username, password string) (cmd *Command, err error) { 203 | if c.Caps["LOGINDISABLED"] { 204 | return nil, NotAvailableError("LOGIN") 205 | } 206 | cmd, err = Wait(c.Send("LOGIN", c.Quote(username), c.Quote(password))) 207 | if err == nil { 208 | c.setState(Auth) 209 | if cmd.result.Label != "CAPABILITY" { 210 | // Gmail servers send an untagged CAPABILITY response after 211 | // successful authentication. RFC 3501 states that the CAPABILITY 212 | // response code in command completion should be used instead, so we 213 | // ignore the untagged response. 214 | _, err = c.Capability() 215 | } 216 | } 217 | return 218 | } 219 | 220 | // Select opens a mailbox on the server for read-write or read-only access. The 221 | // EXAMINE command is used when readonly is set to true. However, even when 222 | // readonly is false, the server may decide not to give read-write access. The 223 | // server may also change access while the mailbox is open. The current mailbox 224 | // status is available from c.Mailbox while the client is in the Selected state. 225 | // 226 | // This command is synchronous. 227 | func (c *Client) Select(mbox string, readonly bool) (cmd *Command, err error) { 228 | return Wait(c.doSelect(mbox, readonly)) 229 | } 230 | 231 | // Create creates a new mailbox on the server. 232 | func (c *Client) Create(mbox string) (cmd *Command, err error) { 233 | return c.Send("CREATE", c.Quote(UTF7Encode(mbox))) 234 | } 235 | 236 | // Delete permanently removes a mailbox and all of its contents from the server. 237 | func (c *Client) Delete(mbox string) (cmd *Command, err error) { 238 | return c.Send("DELETE", c.Quote(UTF7Encode(mbox))) 239 | } 240 | 241 | // Rename changes the name of a mailbox. 242 | func (c *Client) Rename(old, new string) (cmd *Command, err error) { 243 | return c.Send("RENAME", c.Quote(UTF7Encode(old)), c.Quote(UTF7Encode(new))) 244 | } 245 | 246 | // Subscribe adds the specified mailbox name to the server's set of "active" or 247 | // "subscribed" mailboxes as returned by the LSUB command. 248 | func (c *Client) Subscribe(mbox string) (cmd *Command, err error) { 249 | return c.Send("SUBSCRIBE", c.Quote(UTF7Encode(mbox))) 250 | } 251 | 252 | // Unsubscribe removes the specified mailbox name from the server's set of 253 | // "active" or "subscribed" mailboxes as returned by the LSUB command. 254 | func (c *Client) Unsubscribe(mbox string) (cmd *Command, err error) { 255 | return c.Send("UNSUBSCRIBE", c.Quote(UTF7Encode(mbox))) 256 | } 257 | 258 | // List returns a subset of mailbox names from the complete set of all names 259 | // available to the client. 260 | // 261 | // See RFC 3501 sections 6.3.8 and 7.2.2, and RFC 2683 for detailed information 262 | // about the LIST and LSUB commands. 263 | func (c *Client) List(ref, mbox string) (cmd *Command, err error) { 264 | return c.Send("LIST", c.Quote(ref), c.Quote(mbox)) 265 | } 266 | 267 | // LSub returns a subset of mailbox names from the set of names that the user 268 | // has declared as being "active" or "subscribed". 269 | func (c *Client) LSub(ref, mbox string) (cmd *Command, err error) { 270 | return c.Send("LSUB", c.Quote(ref), c.Quote(mbox)) 271 | } 272 | 273 | // Status requests the status of the indicated mailbox. The currently defined 274 | // status data items that can be requested are: MESSAGES, RECENT, UIDNEXT, 275 | // UIDVALIDITY, and UNSEEN. All data items are requested by default. 276 | func (c *Client) Status(mbox string, items ...string) (cmd *Command, err error) { 277 | var f []Field 278 | if len(items) == 0 { 279 | f = []Field{"MESSAGES", "RECENT", "UIDNEXT", "UIDVALIDITY", "UNSEEN"} 280 | } else { 281 | f = stringsToFields(items) 282 | } 283 | return c.Send("STATUS", c.Quote(UTF7Encode(mbox)), f) 284 | } 285 | 286 | // Append appends the literal argument as a new message to the end of the 287 | // specified destination mailbox. Flags and internal date arguments are optional 288 | // and may be set to nil. 289 | func (c *Client) Append(mbox string, flags FlagSet, idate *time.Time, msg Literal) (cmd *Command, err error) { 290 | f := []Field{c.Quote(UTF7Encode(mbox)), nil, nil, nil}[:1] 291 | if flags != nil { 292 | f = append(f, flags) 293 | } 294 | if idate != nil { 295 | f = append(f, *idate) 296 | } 297 | return c.Send("APPEND", append(f, msg)...) 298 | } 299 | 300 | // Check requests a checkpoint of the currently selected mailbox. A checkpoint 301 | // is an implementation detail of the server and may be equivalent to a NOOP. 302 | func (c *Client) Check() (cmd *Command, err error) { 303 | return c.Send("CHECK") 304 | } 305 | 306 | // Close closes the currently selected mailbox, returning the client to the 307 | // authenticated state. If expunge is true, all messages marked for deletion are 308 | // permanently removed from the mailbox. 309 | // 310 | // If expunge is false and UNSELECT capability is not advertised, the client 311 | // issues the EXAMINE command with a non-existent mailbox name. This closes the 312 | // current mailbox without expunging it, but the "successful" command completion 313 | // status will be NO instead of OK. 314 | // 315 | // This command is synchronous. 316 | func (c *Client) Close(expunge bool) (cmd *Command, err error) { 317 | name := "CLOSE" 318 | if !expunge { 319 | if !c.Caps["UNSELECT"] { 320 | mbox := "GOIMAP" + randStr(6) 321 | if cmd, err = c.doSelect(mbox, true); err == nil { 322 | _, err = cmd.Result(NO) 323 | } 324 | return 325 | } 326 | name = "UNSELECT" 327 | } 328 | if cmd, err = Wait(c.Send(name)); err == nil { 329 | c.setState(Auth) 330 | } 331 | return 332 | } 333 | 334 | // Expunge permanently removes all messages that have the \Deleted flag set from 335 | // the currently selected mailbox. If UIDPLUS capability is advertised, the 336 | // operation can be restricted to messages with specific UIDs by specifying a 337 | // non-nil uids argument. 338 | func (c *Client) Expunge(uids *SeqSet) (cmd *Command, err error) { 339 | if uids != nil { 340 | if !c.Caps["UIDPLUS"] { 341 | return nil, NotAvailableError("UIDPLUS") 342 | } 343 | return c.Send("UID EXPUNGE", uids) 344 | } 345 | return c.Send("EXPUNGE") 346 | } 347 | 348 | // Search searches the mailbox for messages that match the given searching 349 | // criteria. See RFC 3501 section 6.4.4 for a list of all valid search keys. It 350 | // is the caller's responsibility to quote strings when necessary. All strings 351 | // must use UTF-8 encoding. 352 | func (c *Client) Search(spec ...Field) (cmd *Command, err error) { 353 | return c.Send("SEARCH", append([]Field{"CHARSET", "UTF-8"}, spec...)...) 354 | } 355 | 356 | // Fetch retrieves data associated with the specified message(s) in the mailbox. 357 | // See RFC 3501 section 6.4.5 for a list of all valid message data items and 358 | // macros. 359 | func (c *Client) Fetch(seq *SeqSet, items ...string) (cmd *Command, err error) { 360 | return c.Send("FETCH", seq, stringsToFields(items)) 361 | } 362 | 363 | // Store alters data associated with the specified message(s) in the mailbox. 364 | func (c *Client) Store(seq *SeqSet, item string, value Field) (cmd *Command, err error) { 365 | return c.Send("STORE", seq, item, value) 366 | } 367 | 368 | // Copy copies the specified message(s) to the end of the specified destination 369 | // mailbox. 370 | func (c *Client) Copy(seq *SeqSet, mbox string) (cmd *Command, err error) { 371 | return c.Send("COPY", seq, c.Quote(UTF7Encode(mbox))) 372 | } 373 | 374 | // UIDSearch is identical to Search, but the numbers returned in the response 375 | // are unique identifiers instead of message sequence numbers. 376 | func (c *Client) UIDSearch(spec ...Field) (cmd *Command, err error) { 377 | return c.Send("UID SEARCH", append([]Field{"CHARSET", "UTF-8"}, spec...)...) 378 | } 379 | 380 | // UIDFetch is identical to Fetch, but the seq argument is interpreted as 381 | // containing unique identifiers instead of message sequence numbers. 382 | func (c *Client) UIDFetch(seq *SeqSet, items ...string) (cmd *Command, err error) { 383 | return c.Send("UID FETCH", seq, stringsToFields(items)) 384 | } 385 | 386 | // UIDStore is identical to Store, but the seq argument is interpreted as 387 | // containing unique identifiers instead of message sequence numbers. 388 | func (c *Client) UIDStore(seq *SeqSet, item string, value Field) (cmd *Command, err error) { 389 | return c.Send("UID STORE", seq, item, value) 390 | } 391 | 392 | // UIDCopy is identical to Copy, but the seq argument is interpreted as 393 | // containing unique identifiers instead of message sequence numbers. 394 | func (c *Client) UIDCopy(seq *SeqSet, mbox string) (cmd *Command, err error) { 395 | return c.Send("UID COPY", seq, c.Quote(UTF7Encode(mbox))) 396 | } 397 | 398 | // SetQuota changes the resource limits of the specified quota root. See RFC 399 | // 2087 for additional information. 400 | func (c *Client) SetQuota(root string, quota ...*Quota) (cmd *Command, err error) { 401 | if !c.Caps["QUOTA"] { 402 | return nil, NotAvailableError("QUOTA") 403 | } 404 | f := make([]Field, 0, len(quota)*2) 405 | for _, q := range quota { 406 | f = append(f, q.Resource, q.Limit) 407 | } 408 | return c.Send("SETQUOTA", c.Quote(root), f) 409 | } 410 | 411 | // GetQuota returns the quota root's resource usage and limits. See RFC 2087 for 412 | // additional information. 413 | func (c *Client) GetQuota(root string, quota ...*Quota) (cmd *Command, err error) { 414 | if !c.Caps["QUOTA"] { 415 | return nil, NotAvailableError("QUOTA") 416 | } 417 | return c.Send("GETQUOTA", c.Quote(root)) 418 | } 419 | 420 | // GetQuotaRoot returns the list of quota roots for the specified mailbox, and 421 | // the resource usage and limits for each quota root. See RFC 2087 for 422 | // additional information. 423 | func (c *Client) GetQuotaRoot(mbox string) (cmd *Command, err error) { 424 | if !c.Caps["QUOTA"] { 425 | return nil, NotAvailableError("QUOTA") 426 | } 427 | return c.Send("GETQUOTAROOT", c.Quote(UTF7Encode(mbox))) 428 | } 429 | 430 | // Idle places the client into an idle state where the server is free to send 431 | // unsolicited mailbox update messages. No other commands are allowed to run 432 | // while the client is idling. Use c.IdleTerm to terminate the command. See RFC 433 | // 2177 for additional information. 434 | func (c *Client) Idle() (cmd *Command, err error) { 435 | if !c.Caps["IDLE"] { 436 | return nil, NotAvailableError("IDLE") 437 | } 438 | if cmd, err = c.Send("IDLE"); err == nil { 439 | var rsp *Response 440 | if rsp, err = c.checkContinue(cmd, true); err == nil { 441 | if rsp.Type == Continue { 442 | c.Logln(LogState, "Client is idling...") 443 | } else { 444 | _, err = cmd.Result(OK) 445 | } 446 | } 447 | } 448 | return 449 | } 450 | 451 | // IdleTerm terminates the IDLE command. It returns the same Command instance as 452 | // the original Idle call. 453 | func (c *Client) IdleTerm() (cmd *Command, err error) { 454 | if len(c.tags) == 1 { 455 | if cmd = c.cmds[c.tags[0]]; cmd.name == "IDLE" { 456 | if err = c.t.WriteLine([]byte("DONE")); err == nil { 457 | if err = c.t.Flush(); err == nil { 458 | _, err = cmd.Result(OK) 459 | c.Logln(LogState, "Client is done idling") 460 | } 461 | } 462 | } 463 | } 464 | return 465 | } 466 | 467 | // ID provides client identification information to the server. See RFC 2971 for 468 | // additional information. 469 | func (c *Client) ID(info ...string) (cmd *Command, err error) { 470 | if !c.Caps["ID"] { 471 | return nil, NotAvailableError("ID") 472 | } 473 | f := make([]Field, len(info)) 474 | for i, v := range info { 475 | f[i] = c.Quote(v) 476 | } 477 | return c.Send("ID", f) 478 | } 479 | 480 | // CompressDeflate enables data compression using the DEFLATE algorithm. The 481 | // compression level must be between -1 and 9 (see compress/flate). See RFC 4978 482 | // for additional information. 483 | // 484 | // This command is synchronous. 485 | func (c *Client) CompressDeflate(level int) (cmd *Command, err error) { 486 | if !c.Caps["COMPRESS=DEFLATE"] { 487 | return nil, NotAvailableError("COMPRESS=DEFLATE") 488 | } else if c.t.Compressed() { 489 | return nil, ErrCompressionActive 490 | } 491 | if cmd, err = Wait(c.Send("COMPRESS", "DEFLATE")); err == nil { 492 | err = c.t.EnableDeflate(level) 493 | } 494 | return 495 | } 496 | 497 | // Enable takes a list of capability names and requests the server to enable the 498 | // named extensions. See RFC 5161 for additional information. 499 | // 500 | // This command is synchronous. 501 | func (c *Client) Enable(caps ...string) (cmd *Command, err error) { 502 | return Wait(c.Send("ENABLE", stringsToFields(caps))) 503 | } 504 | 505 | // doSelect opens the specified mailbox, returning an error if the command 506 | // completion status is other than OK or NO. 507 | func (c *Client) doSelect(mbox string, readonly bool) (cmd *Command, err error) { 508 | name := "SELECT" 509 | if readonly { 510 | name = "EXAMINE" 511 | } 512 | if cmd, err = c.Send(name, c.Quote(UTF7Encode(mbox))); err == nil { 513 | prev := c.Mailbox 514 | c.setState(Auth) 515 | c.Mailbox = newMailboxStatus(mbox) 516 | 517 | var rsp *Response 518 | if rsp, err = cmd.Result(OK | NO); err == nil { 519 | if rsp.Status == OK { 520 | c.setState(Selected) 521 | } else { 522 | c.Mailbox = nil 523 | } 524 | } else if c.Mailbox = prev; prev != nil && c.state == Auth { 525 | c.setState(Selected) 526 | } 527 | } 528 | return 529 | } 530 | 531 | // stringsToFields converts []string to []Field. 532 | func stringsToFields(s []string) []Field { 533 | f := make([]Field, len(s)) 534 | for i, v := range s { 535 | f[i] = v 536 | } 537 | return f 538 | } 539 | -------------------------------------------------------------------------------- /imap/reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "strconv" 12 | ) 13 | 14 | // ParserError indicates a problem with the server response format. This could 15 | // be the result of an unsupported extension or nonstandard server behavior. 16 | type ParserError struct { 17 | Info string // Short message explaining the problem 18 | Line []byte // Full or partial response line, starting with the tag 19 | Offset int // Parser offset, starting at 0 20 | } 21 | 22 | func (err *ParserError) Error() string { 23 | if err.Line == nil { 24 | return "imap: " + err.Info 25 | } 26 | line, ellipsis := err.Line, "" 27 | if len(line) > rawLimit { 28 | line, ellipsis = line[:rawLimit], "..." 29 | } 30 | return fmt.Sprintf("imap: %s at offset %d of %+q%s", 31 | err.Info, err.Offset, line, ellipsis) 32 | } 33 | 34 | // readerInput is the interface for reading all parts of a response. This 35 | // interface is implemented by transport. 36 | type readerInput interface { 37 | io.Reader 38 | ReadLine() (line []byte, err error) 39 | } 40 | 41 | // reader creates rawResponse structs and provides additional lines and literals 42 | // to the parser when requested. 43 | type reader struct { 44 | readerInput 45 | LiteralReader 46 | 47 | tagid []byte // Tag prefix expected in command completion responses ([A-Z]+) 48 | order int64 // Response order counter 49 | } 50 | 51 | // rawResponse is an intermediate response form used to construct full Response 52 | // objects. The struct returned by reader.Next() contains the start of the next 53 | // response up to the first literal string, if there is one. The parser reads 54 | // literals and additional lines as needed via reader.More(), which appends new 55 | // bytes to line and tail. The current parser position can be calculated as 56 | // len(raw.line) - len(raw.tail). 57 | type rawResponse struct { 58 | *Response 59 | *reader 60 | 61 | line []byte // Full response line without literals or CRLFs 62 | tail []byte // Unconsumed line ending (parser state) 63 | } 64 | 65 | // newReader returns a reader configured to accept tagged responses beginning 66 | // with tagid. 67 | func newReader(in readerInput, lr LiteralReader, tagid string) *reader { 68 | if in == nil || lr == nil || len(tagid) == 0 { 69 | panic("imap: bad arguments to newReader") 70 | } 71 | for _, c := range tagid { 72 | if c < 'A' || c > 'Z' { 73 | panic("imap: bad tagid format") 74 | } 75 | } 76 | return &reader{in, lr, []byte(tagid), 0} 77 | } 78 | 79 | // Next returns the next unparsed server response, or any data read prior to an 80 | // error. If an error is returned and rsp != nil, the connection should be 81 | // terminated because the client and server are no longer synchronized. 82 | func (r *reader) Next() (raw *rawResponse, err error) { 83 | raw = &rawResponse{reader: r} 84 | if raw.line, err = r.ReadLine(); err != nil { 85 | if len(raw.line) == 0 { 86 | raw = nil 87 | } 88 | } else if tag := r.tag(raw.line); tag != "" { 89 | r.order++ 90 | raw.Response = &Response{Order: r.order, Raw: raw.line, Tag: tag} 91 | raw.tail = raw.line[len(tag)+1:] 92 | } else { 93 | err = &ProtocolError{"bad response tag", raw.line} 94 | } 95 | return 96 | } 97 | 98 | // More returns the next literal string and reads one more line from the server. 99 | func (r *reader) More(raw *rawResponse, i LiteralInfo) (l Literal, err error) { 100 | src := io.LimitedReader{R: r, N: int64(i.Len)} 101 | if l, err = r.ReadLiteral(&src, i); l != nil { 102 | raw.Literals = append(raw.Literals, l) 103 | if err == nil { 104 | var line []byte 105 | if line, err = r.ReadLine(); len(line) > 0 { // ok if err != nil 106 | pos := raw.pos() 107 | raw.line = append(raw.line, line...) 108 | raw.tail = raw.line[pos:] 109 | raw.Raw = raw.line 110 | } 111 | } 112 | } else if err == nil { 113 | // Sanity check for user-provided ReadLiteral implementations 114 | panic("imap: ReadLiteral returned (nil, nil)") 115 | } 116 | return 117 | } 118 | 119 | // tag verifies that line is a valid start of a new server response and returns 120 | // the full response tag. Valid tags are "*" (untagged status/data), "+" 121 | // (continuation request), and strings in the format "{r.tagid}[0-9]+" (command 122 | // completion). The tag must be followed by a space. 123 | func (r *reader) tag(line []byte) string { 124 | if n := bytes.IndexByte(line, ' '); n == 1 { 125 | if c := line[0]; c == '*' || c == '+' { 126 | return string(c) 127 | } 128 | } else if i := len(r.tagid); i < n && bytes.Equal(line[:i], r.tagid) { 129 | for _, c := range line[i:n] { 130 | if c < '0' || c > '9' { 131 | return "" 132 | } 133 | } 134 | return string(line[:n]) 135 | } 136 | return "" 137 | } 138 | 139 | // Error returned by parseCondition to indicate that rsp.Type != Status. 140 | var errNotStatus error = &ParserError{Info: "not a status response"} 141 | 142 | // Parse converts rawResponse into a full Response object by calling parseX 143 | // methods, which gradually consume raw.tail. 144 | func (raw *rawResponse) Parse() (rsp *Response, err error) { 145 | if raw.Response == nil { 146 | return nil, &ParserError{"unparsable response", raw.line, 0} 147 | } 148 | switch rsp = raw.Response; rsp.Tag { 149 | case "*": 150 | if err = raw.parseCondition(OK | NO | BAD | PREAUTH | BYE); err == nil { 151 | rsp.Type = Status 152 | err = raw.parseStatus() 153 | } else if err == errNotStatus { 154 | rsp.Type = Data 155 | rsp.Fields, err = raw.parseFields(nul) 156 | if len(rsp.Fields) == 0 && err == nil { 157 | err = raw.error("empty data response", 0) 158 | } 159 | } 160 | case "+": 161 | rsp.Type = Continue 162 | raw.parseContinue() 163 | default: 164 | if err = raw.parseCondition(OK | NO | BAD); err == nil { 165 | rsp.Type = Done 166 | err = raw.parseStatus() 167 | } else if err == errNotStatus { 168 | err = &ParserError{"unknown response type", raw.line, 0} 169 | } 170 | } 171 | if len(raw.tail) > 0 && err == nil { 172 | err = raw.unexpected(0) 173 | } 174 | raw.Response = nil 175 | return 176 | } 177 | 178 | // pos returns the current parser position in raw.line. 179 | func (raw *rawResponse) pos() int { 180 | return len(raw.line) - len(raw.tail) 181 | } 182 | 183 | // error returns a ParserError to indicate a problem with the response at the 184 | // specified offset. The offset is relative to raw.tail. 185 | func (raw *rawResponse) error(info string, off int) error { 186 | return &ParserError{info, raw.line, raw.pos() + off} 187 | } 188 | 189 | // unexpected returns a ParserError to indicate an unexpected byte at the 190 | // specified offset. The offset is relative to raw.tail. 191 | func (raw *rawResponse) unexpected(off int) error { 192 | c := raw.line[raw.pos()+off] 193 | return raw.error(fmt.Sprintf("unexpected %+q", c), off) 194 | } 195 | 196 | // missing returns a ParserError to indicate the absence of a required character 197 | // or section at the specified offset. The offset is relative to raw.tail. 198 | func (raw *rawResponse) missing(v interface{}, off int) error { 199 | if _, ok := v.(byte); ok { 200 | return raw.error(fmt.Sprintf("missing %+q", v), off) 201 | } 202 | return raw.error(fmt.Sprintf("missing %v", v), off) 203 | } 204 | 205 | // Valid status conditions. 206 | var bStatus = []struct { 207 | b []byte 208 | s RespStatus 209 | }{ 210 | {[]byte("OK"), OK}, 211 | {[]byte("NO"), NO}, 212 | {[]byte("BAD"), BAD}, 213 | {[]byte("PREAUTH"), PREAUTH}, 214 | {[]byte("BYE"), BYE}, 215 | } 216 | 217 | // parseCondition extracts the status condition if raw is a status response 218 | // (ABNF: resp-cond-*). errNotStatus is returned for all other response types. 219 | func (raw *rawResponse) parseCondition(accept RespStatus) error { 220 | outer: 221 | for _, v := range bStatus { 222 | if n := len(v.b); n <= len(raw.tail) { 223 | for i, c := range v.b { 224 | if raw.tail[i]&0xDF != c { // &0xDF converts [a-z] to upper case 225 | continue outer 226 | } 227 | } 228 | if n == len(raw.tail) { 229 | return raw.missing("SP", n) 230 | } else if raw.tail[n] == ' ' { 231 | if accept&v.s == 0 { 232 | return raw.error("unacceptable status condition", 0) 233 | } 234 | raw.Status = v.s 235 | raw.tail = raw.tail[n+1:] 236 | return nil 237 | } 238 | // Assume data response with a matching prefix (e.g. "* NOT STATUS") 239 | break 240 | } 241 | } 242 | return errNotStatus 243 | } 244 | 245 | // parseStatus extracts the optional response code and required text after the 246 | // status condition (ABNF: resp-text). 247 | func (raw *rawResponse) parseStatus() error { 248 | if len(raw.tail) > 0 && raw.tail[0] == '[' { 249 | var err error 250 | raw.tail = raw.tail[1:] 251 | if raw.Fields, err = raw.parseFields(']'); err != nil { 252 | return err 253 | } else if len(raw.Fields) == 0 { 254 | return raw.error("empty response code", -1) 255 | } else if len(raw.tail) == 0 { 256 | // Some servers do not send any text after the response code 257 | // (e.g. "* OK [UNSEEN 1]"). This is not allowed, according to RFC 258 | // 3501 ABNF, but we accept it for compatibility with other clients. 259 | raw.tail = nil 260 | return nil 261 | } else if raw.tail[0] != ' ' { 262 | return raw.missing("SP", 0) 263 | } 264 | raw.tail = raw.tail[1:] 265 | } 266 | if len(raw.tail) == 0 { 267 | return raw.missing("status text", 0) 268 | } 269 | raw.Info = string(raw.tail) 270 | raw.tail = nil 271 | return nil 272 | } 273 | 274 | // parseContinue extracts the text or Base64 data from a continuation request 275 | // (ABNF: continue-req). Base64 data is saved in its original form to raw.Info, 276 | // and decoded as []byte into raw.Fields[0]. 277 | func (raw *rawResponse) parseContinue() { 278 | if n := len(raw.tail); n == 0 { 279 | raw.Label = "BASE64" 280 | raw.Fields = []Field{[]byte(nil)} 281 | } else if n&3 == 0 { 282 | if b, err := b64dec(raw.tail); err == nil { 283 | raw.Label = "BASE64" 284 | raw.Fields = []Field{b} 285 | } 286 | } 287 | // ABNF uses resp-text, but section 7.5 states "The remainder of this 288 | // response is a line of text." Assume that response codes are not allowed. 289 | raw.Info = string(raw.tail) 290 | raw.tail = nil 291 | } 292 | 293 | // parseFields extracts as many data fields from raw.tail as possible until it 294 | // finds the stop byte in a delimiter position. An error is returned if the stop 295 | // byte is not found. NUL stop causes all of raw.tail to be consumed (NUL does 296 | // not appear anywhere in raw.line - checked by transport). 297 | func (raw *rawResponse) parseFields(stop byte) (fields []Field, err error) { 298 | if len(raw.tail) > 0 && raw.tail[0] == stop { 299 | // Empty parenthesized list, BODY[] and friends, or an error 300 | raw.tail = raw.tail[1:] 301 | return 302 | } 303 | for len(raw.tail) > 0 && err == nil { 304 | var f Field 305 | switch raw.next() { 306 | case QuotedString: 307 | f, err = raw.parseQuotedString() 308 | case LiteralString: 309 | f, err = raw.parseLiteralString() 310 | case List: 311 | raw.tail = raw.tail[1:] 312 | f, err = raw.parseFields(')') 313 | default: 314 | f, err = raw.parseAtom(raw.Type == Data && stop != ']') 315 | } 316 | if err == nil || f != nil { 317 | fields = append(fields, f) 318 | } 319 | // Delimiter 320 | if len(raw.tail) > 0 && err == nil { 321 | switch raw.tail[0] { 322 | case ' ': 323 | // Allow a space even if it's at the end of the response. Yahoo 324 | // servers send "* SEARCH 2 84 882 " in violation of RFC 3501. 325 | raw.tail = raw.tail[1:] 326 | case stop: 327 | raw.tail = raw.tail[1:] 328 | return 329 | case '(': 330 | // body-type-mpart is 1*body without a space in between 331 | if len(raw.tail) == 1 { 332 | err = raw.unexpected(0) 333 | } 334 | default: 335 | err = raw.unexpected(0) 336 | } 337 | } 338 | } 339 | if stop != nul && err == nil { 340 | err = raw.missing(stop, 0) 341 | } 342 | return 343 | } 344 | 345 | // next returns the type of the next response field. The default type is Atom, 346 | // which includes atoms, numbers, and NILs. 347 | func (raw *rawResponse) next() FieldType { 348 | switch raw.tail[0] { 349 | case '"': 350 | return QuotedString 351 | case '{': 352 | return LiteralString 353 | case '(': 354 | return List 355 | 356 | // RFC 5738 utf8-quoted 357 | case '*': 358 | if len(raw.tail) >= 2 && raw.tail[1] == '"' { 359 | return QuotedString 360 | } 361 | 362 | // RFC 3516 literal8 363 | case '~': 364 | if len(raw.tail) >= 2 && raw.tail[1] == '{' { 365 | return LiteralString 366 | } 367 | } 368 | return Atom 369 | } 370 | 371 | // parseQuotedString returns the next quoted string. The string stays quoted, 372 | // but validation is performed to ensure that subsequent calls to Unquote() are 373 | // successful. 374 | func (raw *rawResponse) parseQuotedString() (f Field, err error) { 375 | start := 1 376 | if raw.tail[0] == '*' { 377 | start++ 378 | } 379 | escaped := false 380 | for n, c := range raw.tail[start:] { 381 | if escaped { 382 | escaped = false 383 | } else if c == '\\' { 384 | escaped = true 385 | } else if c == '"' { 386 | n += start + 1 387 | if _, ok := UnquoteBytes(raw.tail[:n]); ok { 388 | f = string(raw.tail[:n]) 389 | raw.tail = raw.tail[n:] 390 | return 391 | } 392 | break 393 | } 394 | } 395 | err = raw.error("bad quoted string", 0) 396 | return 397 | } 398 | 399 | // parseLiteralString returns the next literal string. The octet count should be 400 | // the last field in raw.tail. An additional line of text will be appended to 401 | // raw.line and raw.tail after the literal is received. 402 | func (raw *rawResponse) parseLiteralString() (f Field, err error) { 403 | var info LiteralInfo 404 | start := 1 405 | if raw.tail[0] == '~' { 406 | info.Bin = true 407 | start++ 408 | } 409 | n := len(raw.tail) - 1 410 | if n-start < 1 || raw.tail[n] != '}' { 411 | err = raw.unexpected(0) 412 | return 413 | } 414 | oc, err := strconv.ParseUint(string(raw.tail[start:n]), 10, 32) 415 | if err != nil { 416 | err = raw.error("bad literal octet count", start) 417 | return 418 | } 419 | info.Len = uint32(oc) 420 | if f, err = raw.More(raw, info); err == nil { 421 | raw.tail = raw.tail[n+1:] 422 | } 423 | return 424 | } 425 | 426 | // atomSpecials identifies ASCII characters that either may not appear in atoms 427 | // or require special handling (ABNF: ATOM-CHAR). 428 | var atomSpecials [char]bool 429 | 430 | func init() { 431 | // atom-specials + '[' to provide special handling for BODY[...] 432 | s := []byte{'(', ')', '{', ' ', '%', '*', '"', '[', '\\', ']', '\x7F'} 433 | for c := byte(0); c < char; c++ { 434 | atomSpecials[c] = c < ctl || bytes.IndexByte(s, c) >= 0 435 | } 436 | } 437 | 438 | // parseAtom returns the next atom, number, or NIL. The syntax rules are relaxed 439 | // to treat sequences such as "BODY[...]<...>" as a single atom. Numbers are 440 | // converted to uint32, NIL is converted to nil, everything else becomes a 441 | // string. Flags (e.g. "\Seen") are converted to title case, other strings are 442 | // left in their original form. 443 | func (raw *rawResponse) parseAtom(astring bool) (f Field, err error) { 444 | n, flag := 0, false 445 | for end := len(raw.tail); n < end; n++ { 446 | if c := raw.tail[n]; c >= char || atomSpecials[c] { 447 | switch c { 448 | case '\\': 449 | if n == 0 { 450 | flag = true 451 | astring = false 452 | continue // ABNF: flag (e.g. `\Seen`) 453 | } 454 | case '*': 455 | if n == 1 && flag { 456 | n++ // ABNF: flag-perm (`\*`), end of atom 457 | } 458 | case '[': 459 | if n == 4 && bytes.EqualFold(raw.tail[:4], []byte("BODY")) { 460 | pos := raw.pos() 461 | raw.tail = raw.tail[n+1:] // Temporary shift for parseFields 462 | 463 | // TODO: Literals between '[' and ']' are handled correctly, 464 | // but only the octet count will make it into the returned 465 | // atom. Would any server actually send a literal here, and 466 | // is it a problem to discard it since the client already 467 | // knows what was requested? 468 | if _, err = raw.parseFields(']'); err != nil { 469 | return 470 | } 471 | n = raw.pos() - pos - 1 472 | raw.tail = raw.line[pos:] // Undo temporary shift 473 | end = len(raw.tail) 474 | astring = false 475 | } 476 | continue // ABNF: fetch-att ("BODY[...]<...>"), atom, or astring 477 | case ']': 478 | if astring { 479 | continue // ABNF: ASTRING-CHAR 480 | } 481 | } 482 | break // raw.tail[n] is a delimiter or an unexpected byte 483 | } 484 | } 485 | 486 | // Atom must have at least one character, two if it starts with a backslash 487 | if n < 2 && (n == 0 || flag) { 488 | err = raw.unexpected(0) 489 | return 490 | } 491 | 492 | // Take whatever was found, let parseFields report delimiter errors 493 | atom := raw.tail[:n] 494 | if norm := normalize(atom); flag { 495 | f = norm 496 | } else if norm != "NIL" { 497 | if c := norm[0]; '0' <= c && c <= '9' { 498 | if ui, err := strconv.ParseUint(norm, 10, 32); err == nil { 499 | f = uint32(ui) 500 | } 501 | } 502 | if f == nil { 503 | if raw.Label == "" { 504 | raw.Label = norm 505 | } 506 | f = string(atom) 507 | } 508 | } 509 | raw.tail = raw.tail[n:] 510 | return 511 | } 512 | 513 | // normalize returns a normalized string copy of an atom. Non-flag atoms are 514 | // converted to upper case. Flags are converted to title case (e.g. `\Seen`). 515 | func normalize(atom []byte) string { 516 | norm := []byte(nil) 517 | want := byte(0) // Want upper case 518 | for i, c := range atom { 519 | have := c & 0x20 520 | if c &= 0xDF; 'A' <= c && c <= 'Z' && have != want { 521 | norm = make([]byte, len(atom)) 522 | break 523 | } else if i == 1 && atom[0] == '\\' { 524 | want = 0x20 // Want lower case starting at i == 2 525 | } 526 | } 527 | if norm == nil { 528 | return string(atom) // Fast path: no changes 529 | } 530 | want = 0 531 | for i, c := range atom { 532 | if c &= 0xDF; 'A' <= c && c <= 'Z' { 533 | norm[i] = c | want 534 | } else { 535 | norm[i] = atom[i] 536 | } 537 | if i == 1 && atom[0] == '\\' { 538 | want = 0x20 539 | } 540 | } 541 | return string(norm) 542 | } 543 | -------------------------------------------------------------------------------- /imap/response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | // Response represents a single status, data, or command continuation response. 13 | // All response types are parsed into the same general format, which can then be 14 | // decoded to more specific representations either by calling the provided 15 | // decoder methods, or by manually navigating Fields and other attributes. Here 16 | // are a few examples of the parser output: 17 | // 18 | // S: * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI 19 | // S: * OK [UNSEEN 12] Message 12 is first unseen 20 | // S: A142 OK [read-write] SELECT completed 21 | // 22 | // Response objects: 23 | // 24 | // &imap.Response{ 25 | // Raw: []byte("* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI"), 26 | // Tag: "*", 27 | // Type: imap.Data, 28 | // Label: "CAPABILITY", 29 | // Fields: []Field{"CAPABILITY", "IMAP4rev1", "STARTTLS", "AUTH=GSSAPI"}, 30 | // } 31 | // &imap.Response{ 32 | // Raw: []byte("* OK [UNSEEN 12] Message 12 is first unseen"), 33 | // Tag: "*", 34 | // Type: imap.Status, 35 | // Status: imap.OK, 36 | // Info: "Message 12 is first unseen", 37 | // Label: "UNSEEN", 38 | // Fields: []Field{"UNSEEN", uint32(12)}, 39 | // } 40 | // &imap.Response{ 41 | // Raw: []byte("A142 OK [read-write] SELECT completed"), 42 | // Tag: "A142", 43 | // Type: imap.Done, 44 | // Status: imap.OK, 45 | // Info: "SELECT completed", 46 | // Label: "READ-WRITE", 47 | // Fields: []Field{"read-write"}, 48 | // } 49 | type Response struct { 50 | // Order in which this response was received, starting at 1 for the server 51 | // greeting. 52 | Order int64 53 | 54 | // Original response line from which this Response object was constructed. 55 | // Literal strings and CRLFs are omitted. 56 | Raw []byte 57 | 58 | // All literal strings in the order they were received. Do not assume that a 59 | // FETCH request for BODY[], for example, will return exactly one literal 60 | // with the requested data. Use the decoder methods, or navigate Fields 61 | // according to the response format, to get the desired information. 62 | Literals []Literal 63 | 64 | // Response tag ("*", "+", or command tag). 65 | Tag string 66 | 67 | // Response type (Status, Data, Continue, or Done). 68 | Type RespType 69 | 70 | // Status condition if Type is Status or Done (OK, NO, BAD, PREAUTH, or 71 | // BYE). Only OK, NO, and BAD may be used in tagged (Done) responses. 72 | Status RespStatus 73 | 74 | // Human-readable text portion of a Status, Continue, or Done response, or 75 | // the original Base64 text of a challenge-response authentication request. 76 | Info string 77 | 78 | // First atom in Fields (usually index 0 or 1) converted to upper case. This 79 | // determines the format of Fields, as described in RFC 3501 section 7. A 80 | // Continue response containing Base64 data is labeled "BASE64". 81 | Label string 82 | 83 | // Data or response code fields extracted by the parser. For a Data 84 | // response, this is everything after the "*" tag. For a Status or Done 85 | // response, this is the response code (if there is one). For a Continue 86 | // response with a "BASE64" Label, Fields[0] is the decoded byte slice. 87 | Fields []Field 88 | 89 | // Cached decoder output. This is used by the decoder methods to avoid 90 | // traversing Fields multiple times. User code should not modify or access 91 | // this field except when writing a custom decoder (see response.go for 92 | // examples). 93 | Decoded interface{} 94 | } 95 | 96 | // String returns the raw text from which this Response object was constructed. 97 | // Literal strings and CRLFs are omitted. 98 | func (rsp *Response) String() string { 99 | return string(rsp.Raw) 100 | } 101 | 102 | // Value returns the first unsigned 32-bit integer in Fields without descending 103 | // into parenthesized lists. This decoder is primarily intended for Status/Data 104 | // responses labeled EXISTS, RECENT, EXPUNGE, UNSEEN, UIDNEXT, and UIDVALIDITY. 105 | func (rsp *Response) Value() uint32 { 106 | v, ok := rsp.Decoded.(uint32) 107 | if !ok && rsp.Decoded == nil { 108 | for _, f := range rsp.Fields { 109 | if TypeOf(f) == Number { 110 | v = AsNumber(f) 111 | rsp.Decoded = v 112 | break 113 | } 114 | } 115 | } 116 | return v 117 | } 118 | 119 | // Challenge returns the decoded Base64 data from a continuation request sent 120 | // during challenge-response authentication. 121 | func (rsp *Response) Challenge() []byte { 122 | v, ok := rsp.Decoded.([]byte) 123 | if !ok && rsp.Decoded == nil && rsp.Label == "BASE64" { 124 | v = AsBytes(rsp.Fields[0]) 125 | rsp.Decoded = v 126 | } 127 | return v 128 | } 129 | 130 | // MailboxInfo represents the mailbox attributes returned in a LIST or LSUB 131 | // response. 132 | type MailboxInfo struct { 133 | Attrs FlagSet // Mailbox attributes (e.g. `\Noinferiors`, `\Noselect`) 134 | Delim string // Hierarchy delimiter (empty string == NIL, i.e. flat name) 135 | Name string // Mailbox name decoded to UTF-8 136 | } 137 | 138 | // MailboxInfo returns the mailbox attributes extracted from a LIST or LSUB 139 | // response. 140 | func (rsp *Response) MailboxInfo() *MailboxInfo { 141 | v, ok := rsp.Decoded.(*MailboxInfo) 142 | if !ok && rsp.Decoded == nil && 143 | (rsp.Label == "LIST" || rsp.Label == "LSUB") { 144 | v = &MailboxInfo{ 145 | Attrs: AsFlagSet(rsp.Fields[1]), 146 | Delim: AsString(rsp.Fields[2]), 147 | Name: AsMailbox(rsp.Fields[3]), 148 | } 149 | rsp.Decoded = v 150 | } 151 | return v 152 | } 153 | 154 | // MailboxStatus represents the mailbox status information returned in a STATUS 155 | // response. It is also used by the Client to keep an updated view of the 156 | // currently selected mailbox. Fields that are only set by the Client are marked 157 | // as client-only. 158 | type MailboxStatus struct { 159 | Name string // Mailbox name 160 | ReadOnly bool // Mailbox read/write access (client-only) 161 | Flags FlagSet // Defined flags in the mailbox (client-only) 162 | PermFlags FlagSet // Flags that the client can change permanently (client-only) 163 | Messages uint32 // Number of messages in the mailbox 164 | Recent uint32 // Number of messages with the \Recent flag set 165 | Unseen uint32 // Sequence number of the first unseen message 166 | UIDNext uint32 // The next unique identifier value 167 | UIDValidity uint32 // The unique identifier validity value 168 | UIDNotSticky bool // UIDPLUS extension (client-only) 169 | } 170 | 171 | // newMailboxStatus returns an initialized MailboxStatus instance. 172 | func newMailboxStatus(name string) *MailboxStatus { 173 | if len(name) == 5 && toUpper(name) == "INBOX" { 174 | name = "INBOX" 175 | } 176 | return &MailboxStatus{ 177 | Name: name, 178 | Flags: make(FlagSet), 179 | PermFlags: make(FlagSet), 180 | } 181 | } 182 | 183 | func (m *MailboxStatus) String() string { 184 | return fmt.Sprintf("--- %+q ---\n"+ 185 | "ReadOnly: %v\n"+ 186 | "Flags: %v\n"+ 187 | "PermFlags: %v\n"+ 188 | "Messages: %v\n"+ 189 | "Recent: %v\n"+ 190 | "Unseen: %v\n"+ 191 | "UIDNext: %v\n"+ 192 | "UIDValidity: %v\n"+ 193 | "UIDNotSticky: %v\n", 194 | m.Name, m.ReadOnly, m.Flags, m.PermFlags, m.Messages, m.Recent, 195 | m.Unseen, m.UIDNext, m.UIDValidity, m.UIDNotSticky) 196 | } 197 | 198 | // MailboxStatus returns the mailbox status information extracted from a STATUS 199 | // response. 200 | func (rsp *Response) MailboxStatus() *MailboxStatus { 201 | v, ok := rsp.Decoded.(*MailboxStatus) 202 | if !ok && rsp.Decoded == nil && rsp.Label == "STATUS" { 203 | v = &MailboxStatus{Name: AsMailbox(rsp.Fields[1])} 204 | f := AsList(rsp.Fields[2]) 205 | for i := 0; i < len(f)-1; i += 2 { 206 | switch n := AsNumber(f[i+1]); toUpper(AsAtom(f[i])) { 207 | case "MESSAGES": 208 | v.Messages = n 209 | case "RECENT": 210 | v.Recent = n 211 | case "UIDNEXT": 212 | v.UIDNext = n 213 | case "UIDVALIDITY": 214 | v.UIDValidity = n 215 | case "UNSEEN": 216 | v.Unseen = n 217 | } 218 | } 219 | rsp.Decoded = v 220 | } 221 | return v 222 | } 223 | 224 | // SearchResults returns a slice of message sequence numbers or UIDs extracted 225 | // from a SEARCH response. 226 | func (rsp *Response) SearchResults() []uint32 { 227 | v, ok := rsp.Decoded.([]uint32) 228 | if !ok && rsp.Decoded == nil && rsp.Label == "SEARCH" { 229 | if len(rsp.Fields) > 1 { 230 | v = make([]uint32, len(rsp.Fields)-1) 231 | for i, f := range rsp.Fields[1:] { 232 | v[i] = AsNumber(f) 233 | } 234 | } 235 | rsp.Decoded = v 236 | } 237 | return v 238 | } 239 | 240 | // MailboxFlags returns a FlagSet extracted from a FLAGS or PERMANENTFLAGS 241 | // response. Note that FLAGS is a Data response, while PERMANENTFLAGS is Status. 242 | func (rsp *Response) MailboxFlags() FlagSet { 243 | v, ok := rsp.Decoded.(FlagSet) 244 | if !ok && rsp.Decoded == nil && 245 | (rsp.Label == "FLAGS" || rsp.Label == "PERMANENTFLAGS") { 246 | v = AsFlagSet(rsp.Fields[1]) 247 | rsp.Decoded = v 248 | } 249 | return v 250 | } 251 | 252 | // MessageInfo represents the message attributes returned in a FETCH response. 253 | // The values of attributes marked optional are valid only if that attribute 254 | // also appears in Attrs (e.g. UID is valid if and only if Attrs["UID"] != nil). 255 | // These attributes are extracted from Attrs purely for convenience. 256 | type MessageInfo struct { 257 | Attrs FieldMap // All returned attributes 258 | Seq uint32 // Message sequence number 259 | UID uint32 // Unique identifier (optional in non-UID FETCH) 260 | Flags FlagSet // Flags that are set for this message (optional) 261 | InternalDate time.Time // Internal to the server message timestamp (optional) 262 | Size uint32 // Message size in bytes (optional) 263 | } 264 | 265 | // MessageInfo returns the message attributes extracted from a FETCH response. 266 | func (rsp *Response) MessageInfo() *MessageInfo { 267 | v, ok := rsp.Decoded.(*MessageInfo) 268 | if !ok && rsp.Decoded == nil && rsp.Label == "FETCH" { 269 | kv := AsFieldMap(rsp.Fields[2]) 270 | v = &MessageInfo{ 271 | Attrs: kv, 272 | Seq: AsNumber(rsp.Fields[0]), 273 | UID: AsNumber(kv["UID"]), 274 | Flags: AsFlagSet(kv["FLAGS"]), 275 | InternalDate: AsDateTime(kv["INTERNALDATE"]), 276 | Size: AsNumber(kv["RFC822.SIZE"]), 277 | } 278 | rsp.Decoded = v 279 | } 280 | return v 281 | } 282 | 283 | // Quota represents a single resource limit on a mailbox quota root returned in 284 | // a QUOTA response, as described in RFC 2087. 285 | type Quota struct { 286 | Resource string // Resource name (e.g. STORAGE, MESSAGE) 287 | Usage uint32 // Current usage (in units of 1024 octets for STORAGE) 288 | Limit uint32 // Current limit 289 | } 290 | 291 | // Quota returns the resource quotas extracted from a QUOTA response. 292 | func (rsp *Response) Quota() (root string, quota []*Quota) { 293 | type vt struct { 294 | root string 295 | quota []*Quota 296 | } 297 | v, ok := rsp.Decoded.(*vt) 298 | if !ok && rsp.Decoded == nil && rsp.Label == "QUOTA" { 299 | list := AsList(rsp.Fields[2]) 300 | if len(list)%3 != 0 { 301 | return 302 | } 303 | root = AsString(rsp.Fields[1]) 304 | quota = make([]*Quota, len(list)/3) 305 | for i := 0; i < len(list); i += 3 { 306 | quota[i/3] = &Quota{ 307 | Resource: toUpper(AsAtom(list[i])), 308 | Usage: AsNumber(list[i+1]), 309 | Limit: AsNumber(list[i+2]), 310 | } 311 | } 312 | rsp.Decoded = &vt{root, quota} 313 | } else if ok { 314 | root, quota = v.root, v.quota 315 | } 316 | return 317 | } 318 | 319 | // QuotaRoot returns the mailbox name and associated quota roots from a 320 | // QUOTAROOT response. 321 | func (rsp *Response) QuotaRoot() (mbox string, roots []string) { 322 | type vt struct { 323 | mbox string 324 | roots []string 325 | } 326 | v, ok := rsp.Decoded.(*vt) 327 | if !ok && rsp.Decoded == nil && rsp.Label == "QUOTAROOT" { 328 | mbox = AsMailbox(rsp.Fields[1]) 329 | roots = make([]string, len(rsp.Fields[2:])) 330 | for i, root := range rsp.Fields[2:] { 331 | roots[i] = AsString(root) 332 | } 333 | rsp.Decoded = &vt{mbox, roots} 334 | } else if ok { 335 | mbox, roots = v.mbox, v.roots 336 | } 337 | return 338 | } 339 | 340 | // ResponseError wraps a Response pointer for use in an error context, such as 341 | // when a command fails with a NO or BAD status condition. For Status and Done 342 | // response types, the value of Response.Info may be presented to the user. 343 | // Reason provides additional information about the cause of the error. 344 | type ResponseError struct { 345 | *Response 346 | Reason string 347 | } 348 | 349 | func (rsp ResponseError) Error() string { 350 | line, ellipsis := rsp.Raw, "" 351 | if len(line) > rawLimit { 352 | line, ellipsis = line[:rawLimit], "..." 353 | } 354 | return fmt.Sprintf("imap: %s (%+q%s)", rsp.Reason, line, ellipsis) 355 | } 356 | -------------------------------------------------------------------------------- /imap/response_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var ( 14 | UTC = time.FixedZone("", 0) 15 | MST = time.FixedZone("", -7*60*60) 16 | ) 17 | 18 | func TestResponseDecoders(t *testing.T) { 19 | tests := []struct { 20 | in string 21 | call string 22 | out interface{} 23 | }{ 24 | // Original string 25 | {`A142 OK [READ-WRITE] SELECT completed`, 26 | "String", "A142 OK [READ-WRITE] SELECT completed"}, 27 | 28 | // Numeric value -> uint32 29 | {`* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)`, 30 | "Value", uint32(0)}, 31 | {`* 172 EXISTS`, 32 | "Value", uint32(172)}, 33 | {`* 1 RECENT`, 34 | "Value", uint32(1)}, 35 | {`* 22 EXPUNGE`, 36 | "Value", uint32(22)}, 37 | {`* OK [UNSEEN 12] Message 12 is first unseen`, 38 | "Value", uint32(12)}, 39 | {`* OK [UIDNEXT 4392] Predicted next UID`, 40 | "Value", uint32(4392)}, 41 | {`* OK [UIDVALIDITY 3857529045] UIDs valid`, 42 | "Value", uint32(3857529045)}, 43 | 44 | // Authentication challenge -> []byte 45 | {`+ Welcome!`, 46 | "Challenge", []byte(nil)}, 47 | {`+ YDMGCSqGSIb3EgECAgIBAAD/////6jcyG4GE3KkTzBeBiVHeceP2CWY0SR0fAQAgAAQEBAQ=`, 48 | "Challenge", []byte("\x60\x33\x06\x09\x2A\x86\x48\x86\xF7\x12\x01" + 49 | "\x02\x02\x02\x01\x00\x00\xFF\xFF\xFF\xFF\xEA\x37\x32\x1B\x81" + 50 | "\x84\xDC\xA9\x13\xCC\x17\x81\x89\x51\xDE\x71\xE3\xF6\x09\x66" + 51 | "\x34\x49\x1D\x1F\x01\x00\x20\x00\x04\x04\x04\x04")}, 52 | 53 | // LIST and LSUB -> MailboxInfo 54 | {`* NOT LIST`, 55 | "MailboxInfo", (*MailboxInfo)(nil)}, 56 | {`* LIST () NIL ""`, 57 | "MailboxInfo", &MailboxInfo{ 58 | Attrs: NewFlagSet()}}, 59 | {`* LIST () "\\" iNbOx`, 60 | "MailboxInfo", &MailboxInfo{ 61 | Attrs: NewFlagSet(), 62 | Delim: `\`, 63 | Name: "INBOX"}}, 64 | {`* LSUB () "/" blurdybloop`, 65 | "MailboxInfo", &MailboxInfo{ 66 | Attrs: NewFlagSet(), 67 | Delim: "/", 68 | Name: "blurdybloop"}}, 69 | {`* LSUB () "/" [blurdybloop]`, 70 | "MailboxInfo", &MailboxInfo{ 71 | Attrs: NewFlagSet(), 72 | Delim: "/", 73 | Name: "[blurdybloop]"}}, 74 | {`* LIST (\Noselect) "." "#foo.bar"`, 75 | "MailboxInfo", &MailboxInfo{ 76 | Attrs: NewFlagSet(`\Noselect`), 77 | Delim: ".", 78 | Name: "#foo.bar"}}, 79 | {`* LIST (\Noselect) "/" #foo.bar/[blurdybloop]`, 80 | "MailboxInfo", &MailboxInfo{ 81 | Attrs: NewFlagSet(`\Noselect`), 82 | Delim: "/", 83 | Name: "#foo.bar/[blurdybloop]"}}, 84 | {`* LIST (\NoInferiors \NoSelect) NIL {6}` + CRLF + `foobar`, 85 | "MailboxInfo", &MailboxInfo{ 86 | Attrs: NewFlagSet(`\Noselect`, `\Noinferiors`), 87 | Delim: "", 88 | Name: "foobar"}}, 89 | {`* LSUB (\noselect \marked) "/" ~peter/mail/&U,BTFw-/&ZeVnLIqe-`, 90 | "MailboxInfo", &MailboxInfo{ 91 | Attrs: NewFlagSet(`\Noselect`, `\Marked`), 92 | Delim: "/", 93 | Name: "~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E"}}, 94 | 95 | // STATUS -> MailboxStatus 96 | {`* NOT STATUS`, 97 | "MailboxStatus", (*MailboxStatus)(nil)}, 98 | {`* STATUS mailbox ()`, 99 | "MailboxStatus", &MailboxStatus{ 100 | Name: "mailbox"}}, 101 | {`* STATUS inbox (MESSAGES 0)`, 102 | "MailboxStatus", &MailboxStatus{ 103 | Name: "INBOX"}}, 104 | {`* STATUS "inbox" (MESSAGES 1)`, 105 | "MailboxStatus", &MailboxStatus{ 106 | Name: "INBOX", 107 | Messages: 1}}, 108 | {`* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)`, 109 | "MailboxStatus", &MailboxStatus{ 110 | Name: "blurdybloop", 111 | Messages: 231, 112 | UIDNext: 44292}}, 113 | {`* STATUS *"` + "\u263A!" + `" (MESSAGES 10 RECENT 2 UIDNEXT 42 UIDVALIDITY 123 UNSEEN 5)`, 114 | "MailboxStatus", &MailboxStatus{ 115 | Name: "\u263A!", 116 | Messages: 10, 117 | Recent: 2, 118 | UIDNext: 42, 119 | UIDValidity: 123, 120 | Unseen: 5}}, 121 | 122 | // SEARCH -> []uint32 123 | {`* NOT SEARCH`, 124 | "SearchResults", []uint32(nil)}, 125 | {`* SEARCH`, 126 | "SearchResults", []uint32(nil)}, 127 | {`* SEARCH 1`, 128 | "SearchResults", []uint32{1}}, 129 | {`* SEARCH 1 2`, 130 | "SearchResults", []uint32{1, 2}}, 131 | {`* SEARCH 2 3 6`, 132 | "SearchResults", []uint32{2, 3, 6}}, 133 | 134 | // FLAGS and PERMANENTFLAGS -> FlagSet 135 | {`* NOT FLAGS`, 136 | "MailboxFlags", FlagSet(nil)}, 137 | {`* FLAGS ()`, 138 | "MailboxFlags", NewFlagSet()}, 139 | {`* OK [PERMANENTFLAGS ()] No permanent flags permitted`, 140 | "MailboxFlags", NewFlagSet()}, 141 | {`* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)`, 142 | "MailboxFlags", NewFlagSet(`\Answered`, `\Flagged`, `\Deleted`, `\Seen`, `\Draft`)}, 143 | {`* OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited`, 144 | "MailboxFlags", NewFlagSet(`\Deleted`, `\Seen`, `\*`)}, 145 | 146 | // FETCH -> MessageInfo 147 | {`* 0 NOT FETCH`, 148 | "MessageInfo", (*MessageInfo)(nil)}, 149 | {`* 1 FETCH ()`, 150 | "MessageInfo", &MessageInfo{ 151 | Attrs: FieldMap{}, 152 | Seq: 1}}, 153 | {`* 14 FETCH (FLAGS (\Seen \Deleted))`, 154 | "MessageInfo", &MessageInfo{ 155 | Attrs: FieldMap{"FLAGS": []Field{`\Seen`, `\Deleted`}}, 156 | Seq: 14, 157 | Flags: NewFlagSet(`\Seen`, `\Deleted`)}}, 158 | {`* 23 FETCH (FLAGS (\Seen) UID 4827313)`, 159 | "MessageInfo", &MessageInfo{ 160 | Attrs: FieldMap{"FLAGS": []Field{`\Seen`}, "UID": uint32(4827313)}, 161 | Seq: 23, 162 | UID: 4827313, 163 | Flags: NewFlagSet(`\Seen`)}}, 164 | {`* 123 FETCH (INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 44827)`, 165 | "MessageInfo", &MessageInfo{ 166 | Attrs: FieldMap{"INTERNALDATE": `"17-Jul-1996 02:44:25 -0700"`, "RFC822.SIZE": uint32(44827)}, 167 | Seq: 123, 168 | InternalDate: time.Date(1996, time.July, 17, 2, 44, 25, 0, MST), 169 | Size: 44827}}, 170 | {`* 4294967295 FETCH (INTERNALDATE " 7-Jul-1996 02:44:25 +0000")`, 171 | "MessageInfo", &MessageInfo{ 172 | Attrs: FieldMap{"INTERNALDATE": `" 7-Jul-1996 02:44:25 +0000"`}, 173 | Seq: 4294967295, 174 | InternalDate: time.Date(1996, time.July, 7, 2, 44, 25, 0, UTC)}}, 175 | {`* 12 FETCH (body[header] {342}` + CRLF + header + ` UID 1 FLAGS () INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 1024)`, 176 | "MessageInfo", &MessageInfo{ 177 | Attrs: FieldMap{"BODY[HEADER]": lit(header), "UID": uint32(1), "FLAGS": []Field(nil), "INTERNALDATE": `"17-Jul-1996 02:44:25 -0700"`, "RFC822.SIZE": uint32(1024)}, 178 | Seq: 12, 179 | UID: 1, 180 | Flags: NewFlagSet(), 181 | InternalDate: time.Date(1996, time.July, 17, 2, 44, 25, 0, MST), 182 | Size: 1024}}, 183 | 184 | // QUOTA -> (string, []*Quota) 185 | {`* NOT QUOTA`, 186 | "Quota", []interface{}{ 187 | "", []*Quota(nil)}}, 188 | {`* QUOTA "" ()`, 189 | "Quota", []interface{}{ 190 | "", []*Quota{}}}, 191 | {`* QUOTA "" (STORAGE 10 512)`, 192 | "Quota", []interface{}{ 193 | "", []*Quota{&Quota{"STORAGE", 10, 512}}}}, 194 | {`* QUOTA "" (STORAGE 10 512 MESSAGE 20 100)`, 195 | "Quota", []interface{}{ 196 | "", []*Quota{&Quota{"STORAGE", 10, 512}, &Quota{"MESSAGE", 20, 100}}}}, 197 | {`* QUOTA "inbox" (storage 10 512 message 20 100)`, 198 | "Quota", []interface{}{ 199 | "inbox", []*Quota{&Quota{"STORAGE", 10, 512}, &Quota{"MESSAGE", 20, 100}}}}, 200 | 201 | // QUOTAROOT -> (string, []string) 202 | {`* NOT QUOTAROOT`, 203 | "QuotaRoot", []interface{}{ 204 | "", []string(nil)}}, 205 | {`* QUOTAROOT comp.mail.mime`, 206 | "QuotaRoot", []interface{}{ 207 | "comp.mail.mime", []string{}}}, 208 | {`* QUOTAROOT INBOX ""`, 209 | "QuotaRoot", []interface{}{ 210 | "INBOX", []string{""}}}, 211 | {`* QUOTAROOT "inbox" root1 "root2"`, 212 | "QuotaRoot", []interface{}{ 213 | "INBOX", []string{"root1", "root2"}}}, 214 | } 215 | c, s := newTestConn(1024) 216 | C := newTransport(c, nil) 217 | r := newReader(C, MemoryReader{}, "A") 218 | 219 | for _, test := range tests { 220 | C.clear() 221 | s.Write([]byte(test.in + CRLF)) 222 | 223 | raw, err := r.Next() 224 | rsp, err := raw.Parse() 225 | if err != nil { 226 | t.Errorf("Parse(%+q) unexpected error; %v", test.in, err) 227 | continue 228 | } 229 | 230 | vout := reflect.ValueOf(rsp).MethodByName(test.call).Call(nil) 231 | out := make([]interface{}, len(vout)) 232 | for i, v := range vout { 233 | out[i] = v.Interface() 234 | } 235 | if len(out) == 1 { 236 | if !reflect.DeepEqual(out[0], test.out) { 237 | t.Errorf("%s(%+q) expected\n%v; got\n%v", test.call, test.in, test.out, out[0]) 238 | } 239 | } else if !reflect.DeepEqual(out, test.out) { 240 | t.Errorf("%s(%+q) expected\n%v; got\n%v", test.call, test.in, test.out, out) 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /imap/sasl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import "errors" 8 | 9 | // Note: 10 | // Most of this code was copied, with some modifications, from net/smtp. It 11 | // would be better if Go provided a standard package (e.g. crypto/sasl) that 12 | // could be shared by SMTP, IMAP, and other packages. 13 | 14 | // ServerInfo contains information about the IMAP server with which SASL 15 | // authentication is about to be attempted. 16 | type ServerInfo struct { 17 | Name string // Server name 18 | TLS bool // Encryption status 19 | Auth []string // Supported authentication mechanisms 20 | } 21 | 22 | // SASL is the interface for performing challenge-response authentication. 23 | type SASL interface { 24 | // Start begins SASL authentication with the server. It returns the 25 | // authentication mechanism name and "initial response" data (if required by 26 | // the selected mechanism). A non-nil error causes the client to abort the 27 | // authentication attempt. 28 | // 29 | // A nil ir value is different from a zero-length value. The nil value 30 | // indicates that the selected mechanism does not use an initial response, 31 | // while a zero-length value indicates an empty initial response, which must 32 | // be sent to the server. 33 | Start(s *ServerInfo) (mech string, ir []byte, err error) 34 | 35 | // Next continues challenge-response authentication. A non-nil error causes 36 | // the client to abort the authentication attempt. 37 | Next(challenge []byte) (response []byte, err error) 38 | } 39 | 40 | type externalAuth []byte 41 | 42 | // ExternalAuth returns an implementation of the EXTERNAL authentication 43 | // mechanism, as described in RFC 4422. Authorization identity may be left blank 44 | // to indicate that the client is requesting to act as the identity associated 45 | // with the authentication credentials. 46 | func ExternalAuth(identity string) SASL { 47 | return externalAuth(identity) 48 | } 49 | 50 | func (a externalAuth) Start(s *ServerInfo) (mech string, ir []byte, err error) { 51 | return "EXTERNAL", a, nil 52 | } 53 | 54 | func (a externalAuth) Next(challenge []byte) (response []byte, err error) { 55 | return nil, errors.New("unexpected server challenge") 56 | } 57 | 58 | type plainAuth []byte 59 | 60 | // PlainAuth returns an implementation of the PLAIN authentication mechanism, as 61 | // described in RFC 4616. Authorization identity may be left blank to indicate 62 | // that it is the same as the username. 63 | func PlainAuth(username, password, identity string) SASL { 64 | return plainAuth(identity + "\x00" + username + "\x00" + password) 65 | } 66 | 67 | func (a plainAuth) Start(s *ServerInfo) (mech string, ir []byte, err error) { 68 | if !s.TLS { 69 | err = NotAvailableError("AUTH=PLAIN") 70 | } else { 71 | mech, ir = "PLAIN", a 72 | } 73 | return 74 | } 75 | 76 | func (a plainAuth) Next(challenge []byte) (response []byte, err error) { 77 | return nil, errors.New("unexpected server challenge") 78 | } 79 | -------------------------------------------------------------------------------- /imap/seqset.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "fmt" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // SeqSetError is used to report problems with the format of a sequence set 14 | // value. 15 | type SeqSetError string 16 | 17 | func (err SeqSetError) Error() string { 18 | return fmt.Sprintf("imap: bad sequence set value %q", string(err)) 19 | } 20 | 21 | // seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values 22 | // may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is 23 | // represented by setting start = stop. Zero is used to represent "*", which is 24 | // safe because seq-number uses nz-number rule. The order of values is always 25 | // start <= stop, except when representing "n:*", where start = n and stop = 0. 26 | type seq struct { 27 | start, stop uint32 28 | } 29 | 30 | // parseSeqNumber parses a single seq-number value (non-zero uint32 or "*"). 31 | func parseSeqNumber(v string) (uint32, error) { 32 | if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' { 33 | return uint32(n), nil 34 | } else if v == "*" { 35 | return 0, nil 36 | } 37 | return 0, SeqSetError(v) 38 | } 39 | 40 | // parseSeq creates a new seq instance by parsing strings in the format "n" or 41 | // "n:m", where n and/or m may be "*". An error is returned for invalid values. 42 | func parseSeq(v string) (s seq, err error) { 43 | if sep := strings.IndexRune(v, ':'); sep < 0 { 44 | s.start, err = parseSeqNumber(v) 45 | s.stop = s.start 46 | return 47 | } else if s.start, err = parseSeqNumber(v[:sep]); err == nil { 48 | if s.stop, err = parseSeqNumber(v[sep+1:]); err == nil { 49 | if (s.stop < s.start && s.stop != 0) || s.start == 0 { 50 | s.start, s.stop = s.stop, s.start 51 | } 52 | return 53 | } 54 | } 55 | return s, SeqSetError(v) 56 | } 57 | 58 | // Contains returns true if the seq-number q is contained in sequence value s. 59 | // The dynamic value "*" contains only other "*" values, the dynamic range "n:*" 60 | // contains "*" and all numbers >= n. 61 | func (s seq) Contains(q uint32) bool { 62 | if q == 0 { 63 | return s.stop == 0 // "*" is contained only in "*" and "n:*" 64 | } 65 | return s.start != 0 && s.start <= q && (q <= s.stop || s.stop == 0) 66 | } 67 | 68 | // Less returns true if s precedes and does not contain seq-number q. 69 | func (s seq) Less(q uint32) bool { 70 | return (s.stop < q || q == 0) && s.stop != 0 71 | } 72 | 73 | // Merge combines sequence values s and t into a single union if the two 74 | // intersect or one is a superset of the other. The order of s and t does not 75 | // matter. If the values cannot be merged, s is returned unmodified and ok is 76 | // set to false. 77 | func (s seq) Merge(t seq) (union seq, ok bool) { 78 | if union = s; s == t { 79 | ok = true 80 | return 81 | } 82 | if s.start != 0 && t.start != 0 { 83 | // s and t are any combination of "n", "n:m", or "n:*" 84 | if s.start > t.start { 85 | s, t = t, s 86 | } 87 | // s starts at or before t, check where it ends 88 | if (s.stop >= t.stop && t.stop != 0) || s.stop == 0 { 89 | return s, true // s is a superset of t 90 | } 91 | // s is "n" or "n:m", if m == ^uint32(0) then t is "n:*" 92 | if s.stop+1 >= t.start || s.stop == ^uint32(0) { 93 | return seq{s.start, t.stop}, true // s intersects or touches t 94 | } 95 | return 96 | } 97 | // exactly one of s and t is "*" 98 | if s.start == 0 { 99 | if t.stop == 0 { 100 | return t, true // s is "*", t is "n:*" 101 | } 102 | } else if s.stop == 0 { 103 | return s, true // s is "n:*", t is "*" 104 | } 105 | return 106 | } 107 | 108 | // String returns sequence value s as a seq-number or seq-range string. 109 | func (s seq) String() string { 110 | if s.start == s.stop { 111 | if s.start == 0 { 112 | return "*" 113 | } 114 | return strconv.FormatUint(uint64(s.start), 10) 115 | } 116 | b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.start), 10) 117 | if s.stop == 0 { 118 | return string(append(b, ':', '*')) 119 | } 120 | return string(strconv.AppendUint(append(b, ':'), uint64(s.stop), 10)) 121 | } 122 | 123 | // SeqSet is used to represent a set of message sequence numbers or UIDs (see 124 | // sequence-set ABNF rule). The zero value is an empty set. 125 | type SeqSet struct { 126 | set []seq 127 | } 128 | 129 | // NewSeqSet returns a new SeqSet instance after parsing the set string. 130 | func NewSeqSet(set string) (s *SeqSet, err error) { 131 | s = new(SeqSet) 132 | return s, s.Add(set) 133 | } 134 | 135 | // Add inserts new sequence values into the set. The string format is described 136 | // by RFC 3501 sequence-set ABNF rule. If an error is encountered, all values 137 | // inserted successfully prior to the error remain in the set. 138 | func (s *SeqSet) Add(set string) error { 139 | for _, sv := range strings.Split(set, ",") { 140 | v, err := parseSeq(sv) 141 | if err != nil { 142 | return err 143 | } 144 | s.insert(v) 145 | } 146 | return nil 147 | } 148 | 149 | // AddNum inserts new sequence numbers into the set. The value 0 represents "*". 150 | func (s *SeqSet) AddNum(q ...uint32) { 151 | for _, v := range q { 152 | s.insert(seq{v, v}) 153 | } 154 | } 155 | 156 | // AddRange inserts a new sequence range into the set. 157 | func (s *SeqSet) AddRange(start, stop uint32) { 158 | if (stop < start && stop != 0) || start == 0 { 159 | s.insert(seq{stop, start}) 160 | } else { 161 | s.insert(seq{start, stop}) 162 | } 163 | } 164 | 165 | // AddSet inserts all values from t into s. 166 | func (s *SeqSet) AddSet(t *SeqSet) { 167 | for _, v := range t.set { 168 | s.insert(v) 169 | } 170 | } 171 | 172 | // Clear removes all values from the set. 173 | func (s *SeqSet) Clear() { 174 | s.set = s.set[:0] 175 | } 176 | 177 | // Empty returns true if the sequence set does not contain any values. 178 | func (s SeqSet) Empty() bool { 179 | return len(s.set) == 0 180 | } 181 | 182 | // Dynamic returns true if the set contains "*" or "n:*" values. 183 | func (s SeqSet) Dynamic() bool { 184 | return len(s.set) > 0 && s.set[len(s.set)-1].stop == 0 185 | } 186 | 187 | // Contains returns true if the non-zero sequence number or UID q is contained 188 | // in the set. The dynamic range "n:*" contains all q >= n. It is the caller's 189 | // responsibility to handle the special case where q is the maximum UID in the 190 | // mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since 191 | // it doesn't know what the maximum value is). 192 | func (s SeqSet) Contains(q uint32) bool { 193 | if _, ok := s.search(q); ok { 194 | return q != 0 195 | } 196 | return false 197 | } 198 | 199 | // String returns a sorted representation of all contained sequence values. 200 | func (s SeqSet) String() string { 201 | if len(s.set) == 0 { 202 | return "" 203 | } 204 | b := make([]byte, 0, 64) 205 | for _, v := range s.set { 206 | b = append(b, ',') 207 | if v.start == 0 { 208 | b = append(b, '*') 209 | continue 210 | } 211 | b = strconv.AppendUint(b, uint64(v.start), 10) 212 | if v.start != v.stop { 213 | if v.stop == 0 { 214 | b = append(b, ':', '*') 215 | continue 216 | } 217 | b = strconv.AppendUint(append(b, ':'), uint64(v.stop), 10) 218 | } 219 | } 220 | return string(b[1:]) 221 | } 222 | 223 | // insert adds sequence value v to the set. 224 | func (s *SeqSet) insert(v seq) { 225 | i, _ := s.search(v.start) 226 | merged := false 227 | if i > 0 { 228 | // try merging with the preceding entry (e.g. "1,4".insert(2), i == 1) 229 | s.set[i-1], merged = s.set[i-1].Merge(v) 230 | } 231 | if i == len(s.set) { 232 | // v was either merged with the last entry or needs to be appended 233 | if !merged { 234 | s.insertAt(i, v) 235 | } 236 | return 237 | } else if merged { 238 | i-- 239 | } else if s.set[i], merged = s.set[i].Merge(v); !merged { 240 | s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1) 241 | return 242 | } 243 | // v was merged with s.set[i], continue trying to merge until the end 244 | for j := i + 1; j < len(s.set); j++ { 245 | if s.set[i], merged = s.set[i].Merge(s.set[j]); !merged { 246 | if j > i+1 { 247 | // cut out all entries between i and j that were merged 248 | s.set = append(s.set[:i+1], s.set[j:]...) 249 | } 250 | return 251 | } 252 | } 253 | // everything after s.set[i] was merged 254 | s.set = s.set[:i+1] 255 | } 256 | 257 | // insertAt inserts a new sequence value v at index i, resizing s.set as needed. 258 | func (s *SeqSet) insertAt(i int, v seq) { 259 | if n := len(s.set); i == n { 260 | // insert at the end 261 | s.set = append(s.set, v) 262 | return 263 | } else if n < cap(s.set) { 264 | // enough space, shift everything at and after i to the right 265 | s.set = s.set[:n+1] 266 | copy(s.set[i+1:], s.set[i:]) 267 | } else { 268 | // allocate new slice and copy everything, n is at least 1 269 | set := make([]seq, n+1, n*2) 270 | copy(set, s.set[:i]) 271 | copy(set[i+1:], s.set[i:]) 272 | s.set = set 273 | } 274 | s.set[i] = v 275 | return 276 | } 277 | 278 | // search attempts to find the index of the sequence set value that contains q. 279 | // If no values contain q, the returned index is the position where q should be 280 | // inserted and ok is set to false. 281 | func (s SeqSet) search(q uint32) (i int, ok bool) { 282 | min, max := 0, len(s.set)-1 283 | for min < max { 284 | if mid := (min + max) >> 1; s.set[mid].Less(q) { 285 | min = mid + 1 286 | } else { 287 | max = mid 288 | } 289 | } 290 | if max < 0 || s.set[min].Less(q) { 291 | return len(s.set), false // q is the new largest value 292 | } 293 | return min, s.set[min].Contains(q) 294 | } 295 | -------------------------------------------------------------------------------- /imap/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "crypto/tls" 9 | "net" 10 | ) 11 | 12 | // MockServer is an internal type exposed for use by the mock package. 13 | type MockServer interface { 14 | Compressed() bool 15 | Encrypted() bool 16 | Closed() bool 17 | ReadLine() (line []byte, err error) 18 | WriteLine(line []byte) error 19 | Read(p []byte) (n int, err error) 20 | Write(p []byte) (n int, err error) 21 | Flush() error 22 | EnableDeflate(level int) error 23 | EnableTLS(config *tls.Config) error 24 | Close(flush bool) error 25 | } 26 | 27 | // NewMockServer is an internal function exposed for use by the mock package. 28 | func NewMockServer(conn net.Conn) MockServer { 29 | gotest = true 30 | return mockServer{newTransport(conn, nil)} 31 | } 32 | 33 | type mockServer struct{ *transport } 34 | 35 | func (t mockServer) EnableTLS(config *tls.Config) (err error) { 36 | if t.Encrypted() { 37 | return ErrEncryptionActive 38 | } 39 | conn := tls.Server(t.conn, config) 40 | if err = conn.Handshake(); err == nil { 41 | t.conn = conn 42 | if t.Compressed() { 43 | t.cmpLink.Attach(conn, conn) 44 | } else { 45 | t.bufLink.Attach(conn, conn) 46 | } 47 | } 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /imap/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "io" 9 | "unicode/utf8" 10 | ) 11 | 12 | const ( 13 | nul = 0x00 14 | ctl = 0x20 15 | char = 0x80 16 | cr = '\r' 17 | lf = '\n' 18 | ) 19 | 20 | // Quote returns the input as a quoted string for use in a command. An empty 21 | // string is returned if the input cannot be quoted and must be sent as a 22 | // literal. Setting utf8quoted to true indicates server support for utf8-quoted 23 | // string format, as described in RFC 5738. The utf8-quoted form will be used 24 | // only if the input contains non-ASCII characters. 25 | func Quote(s string, utf8quoted bool) string { 26 | return string(QuoteBytes([]byte(s), utf8quoted)) 27 | } 28 | 29 | // QuoteBytes returns the input as a quoted byte slice. Nil is returned if the 30 | // input cannot be quoted and must be sent as a literal. 31 | func QuoteBytes(s []byte, utf8quoted bool) []byte { 32 | escape, unicode := 0, false 33 | for i, n := 0, len(s); i < n; { 34 | if c := s[i]; c < char { 35 | if c == '"' || c == '\\' { 36 | escape++ 37 | } else if c < ctl && (c == nul || c == cr || c == lf) { 38 | return nil 39 | } 40 | i++ 41 | } else if !utf8quoted { 42 | return nil 43 | } else { 44 | _, size := utf8.DecodeRune(s[i:]) 45 | if size == 1 { 46 | return nil 47 | } 48 | unicode = true 49 | i += size 50 | } 51 | } 52 | q := make([]byte, 0, len(s)+escape+3) 53 | if unicode { 54 | q = append(q, '*') 55 | } 56 | q = append(q, '"') 57 | if escape == 0 { 58 | q = append(q, s...) 59 | } else { 60 | for _, c := range s { 61 | if c == '"' || c == '\\' { 62 | q = append(q, '\\', c) 63 | } else { 64 | q = append(q, c) 65 | } 66 | } 67 | } 68 | return append(q, '"') 69 | } 70 | 71 | // Quoted returns true if a string or []byte appears to contain a quoted string, 72 | // based on the presence of surrounding double quotes. The string contents are 73 | // not checked, so it may still contain illegal characters or escape sequences. 74 | // The string may be encoded in utf8-quoted format, as described in RFC 5738. 75 | func Quoted(f Field) bool { 76 | switch s := f.(type) { 77 | case string: 78 | if n := len(s); n >= 2 && s[n-1] == '"' { 79 | return s[0] == '"' || (n >= 3 && s[0] == '*' && s[1] == '"') 80 | } 81 | case []byte: 82 | if n := len(s); n >= 2 && s[n-1] == '"' { 83 | return s[0] == '"' || (n >= 3 && s[0] == '*' && s[1] == '"') 84 | } 85 | } 86 | return false 87 | } 88 | 89 | // QuotedUTF8 returns true if a string or []byte appears to contain a quoted 90 | // string encoded in utf8-quoted format. 91 | func QuotedUTF8(f Field) bool { 92 | switch s := f.(type) { 93 | case string: 94 | n := len(s) 95 | return n >= 3 && s[0] == '*' && s[1] == '"' && s[n-1] == '"' 96 | case []byte: 97 | n := len(s) 98 | return n >= 3 && s[0] == '*' && s[1] == '"' && s[n-1] == '"' 99 | } 100 | return false 101 | } 102 | 103 | // Unquote is the reverse of Quote. An empty string is returned and ok is set to 104 | // false if the input is not a valid quoted string. RFC 3501 specifications are 105 | // relaxed to accept all valid UTF-8 encoded strings with or without the use of 106 | // utf8-quoted format (RFC 5738). Rules disallowing the use of NUL, CR, and LF 107 | // characters still apply. All (and only) double quote and backslash characters 108 | // must be escaped with a backslash. 109 | func Unquote(q string) (s string, ok bool) { 110 | if Quoted(q) { 111 | var b []byte 112 | if b, ok = unquote([]byte(q)); len(b) > 0 { 113 | s = string(b) 114 | } 115 | } 116 | return 117 | } 118 | 119 | // UnquoteBytes is the reverse of QuoteBytes. 120 | func UnquoteBytes(q []byte) (s []byte, ok bool) { 121 | if Quoted(q) { 122 | s, ok = unquote(q) 123 | } 124 | return 125 | } 126 | 127 | // unquote performs the actual unquote operation on a byte slice. It assumes 128 | // that Quoted(q) == true. 129 | func unquote(q []byte) (s []byte, ok bool) { 130 | n := len(q) 131 | if q[0] == '"' { 132 | q = q[1 : n-1] // "..." 133 | } else { 134 | q = q[2 : n-1] // *"..." 135 | } 136 | if n = len(q); n == 0 { 137 | ok = true 138 | return 139 | } 140 | b := make([]byte, 0, n) 141 | for i := 0; i < n; { 142 | if c := q[i]; c < char { 143 | if c == '\\' { 144 | if i++; i == n { 145 | return 146 | } else if c = q[i]; c != '"' && c != '\\' { 147 | return 148 | } 149 | } else if c < ctl && (c == nul || c == cr || c == lf) || c == '"' { 150 | return 151 | } 152 | b = append(b, c) 153 | i++ 154 | } else { 155 | _, size := utf8.DecodeRune(q[i:]) 156 | if size == 1 { 157 | return 158 | } 159 | b = append(b, q[i:i+size]...) 160 | i += size 161 | } 162 | } 163 | return b, true 164 | } 165 | 166 | // LiteralInfo describes the attributes of an incoming or outgoing literal 167 | // string. 168 | type LiteralInfo struct { 169 | Len uint32 // Literal octet count 170 | Bin bool // RFC 3516 literal8 binary format flag 171 | } 172 | 173 | // Literal represents a single incoming or outgoing literal string, as described 174 | // in RFC 3501 section 4.3. Incoming literals are constructed by a 175 | // LiteralReader. The default implementation saves all literals to memory. A 176 | // custom LiteralReader implementation can save literals directly to files. This 177 | // could be advantageous when the client is receiving message bodies containing 178 | // attachments several MB in size. Likewise, a custom Literal implementation can 179 | // transmit outgoing literals by reading directly from files or other data 180 | // sources. 181 | type Literal interface { 182 | // WriteTo writes Info().Length bytes to the Writer w. For the default 183 | // Literal implementation, use AsString or AsBytes field functions to access 184 | // the incoming data directly without copying everything through a Writer. 185 | io.WriterTo 186 | 187 | // Info returns information about the contained literal. 188 | Info() LiteralInfo 189 | } 190 | 191 | // NewLiteral creates a new literal string from a byte slice. The Literal will 192 | // point to the same underlying array as the original slice, so it is not safe 193 | // to modify the array data until the literal has been sent in a command. It is 194 | // the caller's responsibility to create a copy of the data, if needed. 195 | func NewLiteral(b []byte) Literal { 196 | return &literal{b, LiteralInfo{Len: uint32(len(b))}} 197 | } 198 | 199 | // NewLiteral8 creates a new binary literal string from a byte slice. This 200 | // literal is sent using the literal8 syntax, as described in RFC 3516. The 201 | // server must advertise "BINARY" capability for such literals to be accepted. 202 | func NewLiteral8(b []byte) Literal { 203 | return &literal{b, LiteralInfo{Len: uint32(len(b)), Bin: true}} 204 | } 205 | 206 | // literal stores a single literal string in a byte slice. 207 | type literal struct { 208 | data []byte 209 | info LiteralInfo 210 | } 211 | 212 | func (l *literal) WriteTo(w io.Writer) (n int64, err error) { 213 | if len(l.data) == 0 { 214 | return 215 | } 216 | nn, err := w.Write(l.data) 217 | n = int64(nn) 218 | return 219 | } 220 | 221 | func (l *literal) Info() LiteralInfo { 222 | return l.info 223 | } 224 | 225 | // LiteralReader is the interface for receiving literal strings from the server. 226 | // 227 | // ReadLiteral reads exactly i.Length bytes from r into a new literal. It must 228 | // return a Literal instance even when i.Length == 0 (empty string). A return 229 | // value of (nil, nil) is invalid. 230 | type LiteralReader interface { 231 | ReadLiteral(r io.Reader, i LiteralInfo) (Literal, error) 232 | } 233 | 234 | // MemoryReader implements the LiteralReader interface by saving all incoming 235 | // literals to memory. 236 | type MemoryReader struct{} 237 | 238 | func (MemoryReader) ReadLiteral(r io.Reader, i LiteralInfo) (Literal, error) { 239 | if i.Len == 0 { 240 | return &literal{info: i}, nil 241 | } 242 | b := make([]byte, i.Len) 243 | n, err := io.ReadFull(r, b) 244 | return &literal{b[:n], i}, err 245 | } 246 | 247 | // toUpper returns a copy of s with all ASCII characters converted to upper 248 | // case. This is a faster version of strings.ToUpper for ASCII-only strings. 249 | func toUpper(s string) string { 250 | n := len(s) 251 | for i := 0; i < n; i++ { 252 | if c := s[i]; 'a' <= c && c <= 'z' { 253 | goto convert 254 | } 255 | } 256 | return s 257 | 258 | convert: 259 | u := make([]byte, n) 260 | for i := 0; i < n; i++ { 261 | c := s[i] 262 | if 'a' <= c && c <= 'z' { 263 | c &= 0xDF 264 | } 265 | u[i] = c 266 | } 267 | return string(u) 268 | } 269 | -------------------------------------------------------------------------------- /imap/strings_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func q(s string) string { return `"` + s + `"` } 13 | func uq(s string) string { return `*"` + s + `"` } 14 | 15 | var quote_tests = []struct { 16 | in string 17 | utf8 bool 18 | out string 19 | }{ 20 | // Invalid 21 | {"\x00", false, ""}, 22 | {"\x00", true, ""}, 23 | {"\r", false, ""}, 24 | {"\r", true, ""}, 25 | {"\n", false, ""}, 26 | {"\n", true, ""}, 27 | {"\x80", false, ""}, 28 | {"\x80", true, ""}, 29 | {"\xFF", false, ""}, 30 | {"\xFF", true, ""}, 31 | 32 | // ASCII min, max 33 | {"\x01", false, q("\x01")}, 34 | {"\x01", true, q("\x01")}, 35 | {"\x7F", false, q("\x7F")}, 36 | {"\x7F", true, q("\x7F")}, 37 | 38 | // Valid 39 | {``, false, q(``)}, 40 | {`a`, false, q(`a`)}, 41 | {`ab`, false, q(`ab`)}, 42 | {`abc`, true, q(`abc`)}, 43 | {`"`, false, q(`\"`)}, 44 | {`\`, false, q(`\\`)}, 45 | {`""`, false, q(`\"\"`)}, 46 | {`\\`, false, q(`\\\\`)}, 47 | {`"a"`, false, q(`\"a\"`)}, 48 | {`"\"`, false, q(`\"\\\"`)}, 49 | {`"""`, false, q(`\"\"\"`)}, 50 | {`\"\`, false, q(`\\\"\\`)}, 51 | {`"\abc\"`, false, q(`\"\\abc\\\"`)}, 52 | {`"\abc\"`, true, q(`\"\\abc\\\"`)}, 53 | {`hello, world`, false, q(`hello, world`)}, 54 | {`\hello/,/world\`, false, q(`\\hello/,/world\\`)}, 55 | 56 | // Unicode 57 | {"\u65e5", false, ""}, 58 | {"\u65e5", true, uq("\u65e5")}, 59 | {`"\u65e5"`, false, q(`\"\\u65e5\"`)}, 60 | {`"\u65e5"`, true, q(`\"\\u65e5\"`)}, 61 | 62 | {q("\u65e5"), false, ""}, 63 | {q("\u65e5"), true, uq(`\"` + "\u65e5" + `\"`)}, 64 | {q("\u65e5\u672c\u8a9e!"), false, ""}, 65 | {q("\u65e5\u672c\u8a9e!"), true, uq(`\"` + "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e!" + `\"`)}, 66 | 67 | {"\xe6\x97", true, ""}, 68 | {"\xe6\x97\\", true, ""}, 69 | {"\xe6\x97\x00", true, ""}, 70 | {"\xe6\x97\x7f", true, ""}, 71 | {"\xe6\xff\x97\xa5", true, ""}, 72 | {"\xe6\x97\xa5", true, uq("\u65e5")}, 73 | } 74 | 75 | var unquote_tests = []struct { 76 | in string 77 | out string 78 | ok bool 79 | }{ 80 | // Invalid 81 | {``, ``, false}, 82 | {`"`, ``, false}, 83 | {`" `, ``, false}, 84 | {` "`, ``, false}, 85 | {`''`, ``, false}, 86 | {`*"`, ``, false}, 87 | {`*" `, ``, false}, 88 | 89 | {q(`\`), ``, false}, 90 | {uq(`\`), ``, false}, 91 | {q(`"`), ``, false}, 92 | {uq(`"`), ``, false}, 93 | {q(`\\\`), ``, false}, 94 | {uq(`\\\`), ``, false}, 95 | 96 | {q("\x00"), ``, false}, 97 | {q("\r"), ``, false}, 98 | {q("\n"), ``, false}, 99 | {q("\x80"), ``, false}, 100 | {q("\xFF"), ``, false}, 101 | 102 | // Valid 103 | {q(""), "", true}, 104 | {uq(""), "", true}, 105 | {q(" "), " ", true}, 106 | {uq(" "), " ", true}, 107 | {q("'"), "'", true}, 108 | {uq("'"), "'", true}, 109 | {q("abc"), "abc", true}, 110 | {uq("abc"), "abc", true}, 111 | 112 | {q("\x01"), "\x01", true}, 113 | {q("\x7F"), "\x7F", true}, 114 | 115 | {q(`\\`), `\`, true}, 116 | {q(`\"`), `"`, true}, 117 | {q(`\\\\`), `\\`, true}, 118 | {q(`\"\\`), `"\`, true}, 119 | {q(`\\\"`), `\"`, true}, 120 | {q(`\\\\\\`), `\\\`, true}, 121 | {q(`\\\"\\`), `\"\`, true}, 122 | 123 | // Unicode 124 | {q("\u65e5"), "\u65e5", true}, 125 | {uq("\u65e5"), "\u65e5", true}, 126 | 127 | {q("/\u65e5\u672c\u8a9e\\"), "", false}, 128 | {q("/\u65e5\u672c\u8a9e\\\\"), "/\u65e5\u672c\u8a9e\\", true}, 129 | {uq("\u65e5\u672c\u8a9e!"), "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e!", true}, 130 | 131 | {q("\xe6\x97"), "", false}, 132 | {uq("\xe6\x97"), "", false}, 133 | {uq("\xe6\x97\\"), "", false}, 134 | {uq("\xe6\x97\x00"), "", false}, 135 | {uq("\xe6\x97\x7f"), "", false}, 136 | {uq("\xe6\xff\x97\xa5"), "", false}, 137 | {uq("\xe6\x97\xa5"), "\u65e5", true}, 138 | {q("\xe6\x97\xa5"), "\u65e5", true}, 139 | } 140 | 141 | func TestStringsQuote(t *testing.T) { 142 | for _, test := range quote_tests { 143 | out := Quote(test.in, test.utf8) 144 | if out != test.out { 145 | t.Errorf("Quote(%#q, %v) expected %#q; got %#q", test.in, test.utf8, test.out, out) 146 | } 147 | if test.out != "" { 148 | if !Quoted(out) || !Quoted([]byte(out)) { 149 | t.Errorf("Quoted(Quote(%#q)) expected true", test.in) 150 | } else if utf8 := test.out[0] == '*'; QuotedUTF8(out) != utf8 || QuotedUTF8([]byte(out)) != utf8 { 151 | t.Errorf("QuotedUTF8(Quote(%#q)) expected %v", test.in, utf8) 152 | } 153 | } 154 | } 155 | } 156 | 157 | func TestStringsUnquote(t *testing.T) { 158 | for _, test := range unquote_tests { 159 | out, ok := Unquote(test.in) 160 | if out != test.out || ok != test.ok { 161 | t.Errorf("Unquote(%#q) expected %#q (%v); got %#q (%v)", test.in, test.out, test.ok, out, ok) 162 | } 163 | } 164 | } 165 | 166 | func TestStringsQuoteInverse(t *testing.T) { 167 | for _, test := range quote_tests { 168 | if test.out == "" { 169 | continue 170 | } 171 | in, ok := Unquote(Quote(test.in, test.utf8)) 172 | if in != test.in || !ok { 173 | t.Errorf("QuoteInverse(%#q) expected %#q (true); got %#q (%v)", test.in, test.in, in, ok) 174 | } 175 | } 176 | for _, test := range unquote_tests { 177 | if !test.ok { 178 | continue 179 | } 180 | out, ok := Unquote(test.in) 181 | out, ok = Unquote(Quote(out, true)) 182 | if out != test.out || !ok { 183 | t.Errorf("UnquoteInverse(%#q) expected %#q (true); got %#q (%v)", test.in, test.out, out, ok) 184 | } 185 | } 186 | } 187 | 188 | func TestStringsToUpper(t *testing.T) { 189 | // Handling of ASCII bytes is identical to strings.ToUpper 190 | for _, in := range []string{"TEST", "test", "Test", "TeSt", "TesT", "tESt"} { 191 | exp := strings.ToUpper(in) 192 | out := toUpper(in) 193 | if out != exp { 194 | t.Errorf("toUpper(%+q) expected %+q; got %+q", in, exp, out) 195 | } 196 | } 197 | for in := byte(0); in < char; in++ { 198 | exp := strings.ToUpper(string(in)) 199 | out := toUpper(string(in)) 200 | if out != exp { 201 | t.Fatalf("toUpper(%+q) expected %+q; got %+q", string(in), exp, out) 202 | } 203 | } 204 | // Non-ASCII bytes are unaffected 205 | for in := byte(char); in != 0; in++ { 206 | exp := string(in) 207 | out := toUpper(string(in)) 208 | if out != exp { 209 | t.Fatalf("toUpper(%+q) expected %+q; got %+q", string(in), exp, out) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /imap/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "bufio" 9 | "compress/flate" 10 | "crypto/tls" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "net" 15 | ) 16 | 17 | // Labels for identifying the source of log entries. 18 | const ( 19 | client = 'C' 20 | server = 'S' 21 | ) 22 | 23 | // Limit for the maximum number of characters to print in messages containing 24 | // raw command/response lines. 25 | const rawLimit = 1024 26 | 27 | // ProtocolError indicates a low-level problem with the data being sent by the 28 | // client or server. 29 | type ProtocolError struct { 30 | Info string // Short message explaining the problem 31 | Line []byte // Full or partial command/response line 32 | } 33 | 34 | func (err *ProtocolError) Error() string { 35 | if err.Line == nil { 36 | return "imap: " + err.Info 37 | } 38 | line, ellipsis := err.Line, "" 39 | if len(line) > rawLimit { 40 | line, ellipsis = line[:rawLimit], "..." 41 | } 42 | return fmt.Sprintf("imap: %s (%+q%s)", err.Info, line, ellipsis) 43 | } 44 | 45 | // Errors returned by the low-level data transport. 46 | var ( 47 | ErrCompressionActive = errors.New("imap: compression already enabled") 48 | ErrEncryptionActive = errors.New("imap: encryption already enabled") 49 | ) 50 | 51 | // BufferSize sets the size of the send and receive buffers (in bytes). This is 52 | // also the length limit of physical lines. In practice, the client should 53 | // restrict line length to approximately 1000 bytes, as described in RFC 2683. 54 | var BufferSize = 65536 55 | 56 | // Line termination. 57 | var crlf = []byte{cr, lf} 58 | 59 | // ioLink redirects Read/Write operations when compression or encryption become 60 | // enabled. It also keeps track of how many bytes have been transferred via the 61 | // link. 62 | type ioLink struct { 63 | io.Reader 64 | io.Writer 65 | 66 | // Read/write byte count 67 | rc, wc int64 68 | } 69 | 70 | func (l *ioLink) Attach(r io.Reader, w io.Writer) { 71 | l.Reader = r 72 | l.Writer = w 73 | } 74 | 75 | func (l *ioLink) Read(p []byte) (n int, err error) { 76 | n, err = l.Reader.Read(p) 77 | l.rc += int64(n) 78 | return 79 | } 80 | 81 | func (l *ioLink) Write(p []byte) (n int, err error) { 82 | n, err = l.Writer.Write(p) 83 | l.wc += int64(n) 84 | return 85 | } 86 | 87 | func (l *ioLink) Flush() error { 88 | type Flusher interface { 89 | Flush() error 90 | } 91 | if f, ok := l.Writer.(Flusher); ok { 92 | return f.Flush() 93 | } 94 | return nil 95 | } 96 | 97 | func (l *ioLink) Close() error { 98 | var rerr error 99 | if r, ok := l.Reader.(io.Closer); ok { 100 | rerr = r.Close() 101 | } 102 | if w, ok := l.Writer.(io.Closer); ok { 103 | if werr := w.Close(); werr != nil { 104 | return werr 105 | } 106 | } 107 | return rerr 108 | } 109 | 110 | // transport handles low-level communications with the IMAP server. It supports 111 | // optional compression and encryption, which can be enabled at any time and in 112 | // any order. Buffering is provided for incoming and outgoing data. The complete 113 | // data flow is shown in the following diagram (bracketed stages are optional): 114 | // 115 | // transport <--> buffer <--> [compression] <--> [encryption] <--> network 116 | type transport struct { 117 | buf *bufio.ReadWriter // I/O buffer 118 | bufLink *ioLink // Buffer Read/Write provider 119 | cmpLink *ioLink // Compression Read/Write provider 120 | conn net.Conn // Network connection 121 | 122 | // Debug logging 123 | *debugLog 124 | } 125 | 126 | // newTransport wraps an existing network connection in a new transport 127 | // instance. The connection may already be encrypted. 128 | func newTransport(conn net.Conn, log *debugLog) *transport { 129 | lnk := &ioLink{Reader: conn, Writer: conn} 130 | buf := bufio.NewReadWriter( 131 | bufio.NewReaderSize(lnk, BufferSize), 132 | bufio.NewWriterSize(lnk, BufferSize), 133 | ) 134 | return &transport{buf: buf, bufLink: lnk, conn: conn, debugLog: log} 135 | } 136 | 137 | // Compressed returns true if data compression is enabled. 138 | func (t *transport) Compressed() bool { 139 | return t.cmpLink != nil 140 | } 141 | 142 | // Encrypted returns true if data encryption is enabled. 143 | func (t *transport) Encrypted() bool { 144 | _, ok := t.conn.(*tls.Conn) 145 | return ok 146 | } 147 | 148 | // Closed returns true after Close is called on the transport. 149 | func (t *transport) Closed() bool { 150 | return t.conn == nil 151 | } 152 | 153 | // ReadLine returns the next physical line received from the server. The CRLF 154 | // ending is stripped and err is set to nil if and only if the line ends with 155 | // CRLF, and does not contain NUL, CR, or LF characters anywhere else in the 156 | // text. Otherwise, all bytes that have been read are returned unmodified along 157 | // with an error explaining the problem. 158 | func (t *transport) ReadLine() (line []byte, err error) { 159 | line, err = t.buf.ReadSlice(lf) 160 | n := len(line) 161 | 162 | // Copy bytes out of the read buffer 163 | if n > 0 { 164 | temp := make([]byte, n) 165 | copy(temp, line) 166 | line = temp 167 | } else { 168 | line = nil 169 | } 170 | 171 | // Check line format; if err == nil, the line ends with LF 172 | if err == nil { 173 | if n >= 2 && line[n-2] == cr { 174 | line = line[:n-2] 175 | for _, c := range line { 176 | if c < ctl && (c == nul || c == cr) { 177 | line = line[:n] 178 | err = &ProtocolError{"bad line format", line} 179 | break 180 | } 181 | } 182 | } else { 183 | err = &ProtocolError{"bad line ending", line} 184 | } 185 | } else if err == bufio.ErrBufferFull { 186 | err = &ProtocolError{"line too long", line} 187 | } 188 | t.LogLine(server, line, err) 189 | return 190 | } 191 | 192 | // WriteLine writes a physical line to the internal buffer. The CRLF ending is 193 | // appended automatically. The line will not be sent to the server until Flush 194 | // is called or the buffer becomes full from subsequent writes. 195 | func (t *transport) WriteLine(line []byte) error { 196 | var err error 197 | 198 | // Check line format 199 | for _, c := range line { 200 | if c < ctl && (c == nul || c == cr || c == lf) { 201 | err = &ProtocolError{"bad line format", line} 202 | break 203 | } 204 | } 205 | 206 | // Free enough space in the buffer for the entire line 207 | if n := len(line) + 2; n > t.buf.Available() && err == nil { 208 | if err = t.buf.Flush(); n > t.buf.Available() && err == nil { 209 | err = &ProtocolError{"line too long", line} 210 | } 211 | } 212 | 213 | // Write the line followed by CRLF 214 | if err == nil { 215 | if _, err = t.buf.Write(line); err == nil { 216 | _, err = t.buf.Write(crlf) 217 | } 218 | } 219 | t.LogLine(client, line, err) 220 | return err 221 | } 222 | 223 | // Read reads up to len(p) bytes into p. It returns the number of bytes read 224 | // (0 <= n <= len(p)) and any error encountered. 225 | func (t *transport) Read(p []byte) (n int, err error) { 226 | n, err = t.buf.Read(p) 227 | t.LogBytes(server, n, err) 228 | return 229 | } 230 | 231 | // Write writes len(p) bytes from p to the internal buffer. It returns the 232 | // number of bytes written from p (0 <= n <= len(p)) and any error encountered 233 | // that caused the write to stop early. 234 | func (t *transport) Write(p []byte) (n int, err error) { 235 | n, err = t.buf.Write(p) 236 | t.LogBytes(client, n, err) 237 | return 238 | } 239 | 240 | // Flush sends any buffered data to the server. 241 | func (t *transport) Flush() error { 242 | err := t.buf.Flush() 243 | if t.Compressed() && err == nil { 244 | err = t.bufLink.Flush() 245 | } 246 | return err 247 | } 248 | 249 | // EnableDeflate turns on DEFLATE compression. See flate.NewWriter for 250 | // information about compression levels. 251 | func (t *transport) EnableDeflate(level int) error { 252 | if t.Compressed() { 253 | return ErrCompressionActive 254 | } 255 | conn := &ioLink{Reader: t.conn, Writer: t.conn} 256 | inflater := flate.NewReader(conn) 257 | deflater, err := flate.NewWriter(conn, level) 258 | 259 | if err == nil { 260 | t.cmpLink = conn 261 | t.bufLink.Attach(inflater, deflater) 262 | t.Logf(LogConn, "DEFLATE compression enabled (level=%d)", level) 263 | } 264 | return err 265 | } 266 | 267 | // EnableTLS turns on TLS encryption. 268 | func (t *transport) EnableTLS(config *tls.Config) error { 269 | if t.Encrypted() { 270 | return ErrEncryptionActive 271 | } 272 | conn := tls.Client(t.conn, config) 273 | if err := conn.Handshake(); err != nil { 274 | t.Logf(LogConn, "TLS handshake failed (%v)", err) 275 | return err 276 | } 277 | 278 | t.conn = conn 279 | if t.Compressed() { 280 | t.cmpLink.Attach(conn, conn) 281 | } else { 282 | t.bufLink.Attach(conn, conn) 283 | } 284 | state := conn.ConnectionState() 285 | t.Logf(LogConn, "TLS encryption enabled (cipher=0x%04X)", state.CipherSuite) 286 | return nil 287 | } 288 | 289 | // Close terminates the connection. If flush == true, any buffered data is sent 290 | // out before the connection is closed. Calling Flush followed by Close(false) 291 | // is not the same as calling Close(true). Only use Close(false) when no further 292 | // communications are possible (e.g. other side already closed the connection). 293 | func (t *transport) Close(flush bool) error { 294 | if t.Closed() { 295 | return nil 296 | } 297 | conn := t.conn 298 | t.conn = nil 299 | t.Logf(LogConn, "Connection closing (flush=%v)", flush) 300 | 301 | if flush { 302 | err := t.buf.Flush() 303 | if t.Compressed() && err == nil { 304 | err = t.bufLink.Close() 305 | } 306 | if err != nil { 307 | conn.Close() 308 | return err 309 | } 310 | } 311 | return conn.Close() 312 | } 313 | 314 | // LogLine logs a physical line transfer from the client or server. 315 | func (t *transport) LogLine(src byte, line []byte, err error) { 316 | if t.debugLog == nil || t.debugLog.mask&LogRaw != LogRaw { 317 | return 318 | } 319 | ellipsis := "" 320 | if len(line) > rawLimit { 321 | line, ellipsis = line[:rawLimit], "..." 322 | } 323 | if err == nil { 324 | t.Logf(LogRaw, "%c: %s%s", src, line, ellipsis) 325 | return 326 | } 327 | var info string 328 | if pe, ok := err.(*ProtocolError); ok { 329 | info = pe.Info 330 | } else { 331 | info = err.Error() 332 | } 333 | t.Logf(LogRaw, "%c: %s%s (%s)", src, line, ellipsis, info) 334 | } 335 | 336 | // LogBytes logs a literal byte transfer from the client or server. 337 | func (t *transport) LogBytes(src byte, n int, err error) { 338 | if t.debugLog == nil || t.debugLog.mask&LogRaw != LogRaw { 339 | return 340 | } 341 | if err == nil { 342 | t.Logf(LogRaw, "%c: literal %d bytes", src, n) 343 | return 344 | } 345 | t.Logf(LogRaw, "%c: literal %d bytes (%v)", src, n, err) 346 | } 347 | -------------------------------------------------------------------------------- /imap/transport_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/pem" 14 | "io" 15 | "math/big" 16 | "net" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | // Timeout for testConn read/write operations. 22 | var testConnTimeout = 100 * time.Millisecond 23 | 24 | // testConn implements net.Conn via buffered channels. 25 | type testConn struct { 26 | r <-chan byte 27 | w chan<- byte 28 | } 29 | 30 | // testConnError implements net.Error. 31 | type testConnError struct { 32 | err string 33 | timeout bool 34 | temp bool 35 | } 36 | 37 | func (err *testConnError) Error() string { return err.err } 38 | func (err *testConnError) Timeout() bool { return err.timeout } 39 | func (err *testConnError) Temporary() bool { return err.temp } 40 | 41 | // testAddr implements net.Addr. 42 | type testAddr string 43 | 44 | // newTestConn returns two testConn instances representing two sides of a 45 | // network connection. 46 | func newTestConn(bufSize int) (*testConn, *testConn) { 47 | R, W := make(chan byte, bufSize), make(chan byte, bufSize) 48 | return &testConn{r: R, w: W}, &testConn{r: W, w: R} 49 | } 50 | func (c *testConn) Read(b []byte) (n int, err error) { 51 | if ok := false; len(b) > 0 { 52 | select { 53 | case b[n], ok = <-c.r: 54 | if !ok { 55 | return 0, io.EOF 56 | } 57 | n++ 58 | case <-time.After(testConnTimeout): 59 | return 0, &testConnError{err: "testConn: read timeout", timeout: true} 60 | } 61 | for ok && n < len(b) { 62 | select { 63 | case b[n], ok = <-c.r: 64 | if ok { 65 | n++ 66 | } 67 | default: 68 | ok = false 69 | } 70 | } 71 | } 72 | return 73 | } 74 | func (c *testConn) Write(b []byte) (n int, err error) { 75 | if c.w == nil { 76 | return 0, &testConnError{err: "testConn: write to a closed conn"} 77 | } else if len(b) > 0 { 78 | timeout := time.After(testConnTimeout) 79 | for ; n < len(b); n++ { 80 | select { 81 | case c.w <- b[n]: 82 | case <-timeout: 83 | return n, &testConnError{err: "testConn: write timeout", timeout: true} 84 | } 85 | } 86 | } 87 | return 88 | } 89 | func (c *testConn) Clear() { 90 | for ok := true; ok; { 91 | select { 92 | case _, ok = <-c.r: 93 | default: 94 | ok = false 95 | } 96 | } 97 | } 98 | func (c *testConn) Close() error { 99 | if c.w != nil { 100 | close(c.w) 101 | c.w = nil 102 | } 103 | return nil 104 | } 105 | func (c *testConn) LocalAddr() net.Addr { return testAddr("192.0.2.2:32687") } 106 | func (c *testConn) RemoteAddr() net.Addr { return testAddr("192.0.2.1:143") } 107 | func (c *testConn) SetDeadline(t time.Time) error { return nil } 108 | func (c *testConn) SetReadDeadline(t time.Time) error { return nil } 109 | func (c *testConn) SetWriteDeadline(t time.Time) error { return nil } 110 | 111 | func (a testAddr) Network() string { return "test-net-1" } 112 | func (a testAddr) String() string { return string(a) } 113 | 114 | // String-based wrappers for transport methods. 115 | func (t *transport) readln() (s string, err error) { 116 | b, err := t.ReadLine() 117 | return string(b), err 118 | } 119 | func (t *transport) writeln(ln string) error { 120 | return t.WriteLine([]byte(ln)) 121 | } 122 | func (t *transport) read(n int) (s string, err error) { 123 | b := make([]byte, n) 124 | n, err = io.ReadFull(t, b) 125 | return string(b[:n]), err 126 | } 127 | func (t *transport) write(b string) error { 128 | _, err := t.Write([]byte(b)) 129 | return err 130 | } 131 | func (t *transport) send(v ...string) error { 132 | literal := false 133 | for _, in := range v { 134 | if literal { 135 | if err := t.write(in); err != nil { 136 | return err 137 | } 138 | literal = false 139 | } else { 140 | if err := t.writeln(in); err != nil { 141 | return err 142 | } 143 | literal = len(in) > 0 && in[len(in)-1] == '}' 144 | } 145 | } 146 | return t.Flush() 147 | } 148 | func (t *transport) clear() { 149 | if v, ok := t.conn.(*testConn); ok { 150 | v.Clear() 151 | } 152 | t.Read(make([]byte, t.buf.Reader.Buffered())) 153 | } 154 | func (t *transport) starttls(client bool) (err error) { 155 | if client { 156 | err = t.EnableTLS(tlsConfig.client) 157 | } else { 158 | conn := tls.Server(t.conn, tlsConfig.server) 159 | if err = conn.Handshake(); err == nil { 160 | t.conn = conn 161 | if t.Compressed() { 162 | t.cmpLink.Attach(conn, conn) 163 | } else { 164 | t.bufLink.Attach(conn, conn) 165 | } 166 | } 167 | } 168 | return 169 | } 170 | 171 | // TLS client and server configuration. 172 | var tlsConfig = struct { 173 | client *tls.Config 174 | server *tls.Config 175 | }{} 176 | 177 | func init() { 178 | var err error 179 | if tlsConfig.client, tlsConfig.server, err = tlsNewConfig(); err != nil { 180 | panic(err) 181 | } 182 | } 183 | func tlsNewConfig() (client, server *tls.Config, err error) { 184 | now := time.Now() 185 | tpl := x509.Certificate{ 186 | SerialNumber: new(big.Int).SetInt64(0), 187 | Subject: pkix.Name{CommonName: "localhost"}, 188 | NotBefore: now.UTC(), 189 | NotAfter: now.Add(5 * time.Minute).UTC(), 190 | BasicConstraintsValid: true, 191 | IsCA: true, 192 | } 193 | priv, err := rsa.GenerateKey(rand.Reader, 512) 194 | if err != nil { 195 | return 196 | } 197 | crt, err := x509.CreateCertificate(rand.Reader, &tpl, &tpl, &priv.PublicKey, priv) 198 | if err != nil { 199 | return 200 | } 201 | key := x509.MarshalPKCS1PrivateKey(priv) 202 | pair, err := tls.X509KeyPair( 203 | pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: crt}), 204 | pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: key}), 205 | ) 206 | if err != nil { 207 | return 208 | } 209 | root, err := x509.ParseCertificate(crt) 210 | if err == nil { 211 | server = &tls.Config{Certificates: []tls.Certificate{pair}} 212 | client = &tls.Config{RootCAs: x509.NewCertPool(), ServerName: "localhost"} 213 | client.RootCAs.AddCert(root) 214 | } 215 | return 216 | } 217 | 218 | func TestTransportConn(t *testing.T) { 219 | c, s := newTestConn(8) 220 | 221 | // Zero read/write 222 | if n, err := c.Write(nil); n != 0 || err != nil { 223 | t.Fatalf("c.Write(nil) expected 0; got %v (%v)", n, err) 224 | } 225 | if n, err := c.Read(nil); n != 0 || err != nil { 226 | t.Fatalf("c.Read(nil) expected 0; got %v (%v)", n, err) 227 | } 228 | 229 | // Read timeout 230 | if n, err := c.Read([]byte{0}); n != 0 || err == nil { 231 | t.Fatalf("c.Read([]byte{0}) expected 0 (timeout); got %v (%v)", n, err) 232 | } 233 | 234 | // Partial read/write 235 | in := "Abc" 236 | if n, err := s.Write([]byte(in)); n != len(in) || err != nil { 237 | t.Fatalf("s.Write(%q) expected %v; got %v (%v)", in, len(in), n, err) 238 | } 239 | in, b := "A", []byte{0} 240 | for i := 0; i < 2; i++ { 241 | if n, err := c.Read(b); n != len(b) || err != nil || string(b) != in { 242 | t.Fatalf("c.Read(b) expected %q; got %q (%v)", in, b, err) 243 | } 244 | in, b = "bc", []byte{0, 0} 245 | } 246 | 247 | // Full read/write 248 | in = "username" 249 | if n, err := s.Write([]byte(in)); n != len(in) || err != nil { 250 | t.Fatalf("s.Write(%q) expected %v; got %v (%v)", in, len(in), n, err) 251 | } 252 | b = make([]byte, 10) 253 | if n, err := c.Read(b); n != len(in) || err != nil || string(b[:n]) != in { 254 | t.Fatalf("c.Read(b) expected %q; got %q (%v)", in, b[:n], err) 255 | } 256 | 257 | // Write timeout 258 | in = "password*!" 259 | if n, err := s.Write([]byte(in)); n != 8 || err == nil { 260 | t.Fatalf("s.Write(%q) expected 8 (timeout); got %v (%v)", in, n, err) 261 | } 262 | in = "password" 263 | if n, err := c.Read(b); n != len(in) || err != nil || string(b[:n]) != in { 264 | t.Fatalf("c.Read(b) expected %q; got %q (%v)", in, b[:n], err) 265 | } 266 | 267 | // Read timeout 268 | if n, err := c.Read(b); n != 0 || err == nil { 269 | t.Fatalf("c.Read(b) expected 0 (timeout); got %v (%v)", n, err) 270 | } 271 | 272 | // Clear 273 | s.Write([]byte("1 2 3")) 274 | c.Clear() 275 | if n, err := c.Read(b); n != 0 || err == nil { 276 | t.Fatalf("c.Read(b) expected 0 (timeout); got %v (%v)", n, err) 277 | } 278 | 279 | // Close and EOF 280 | s.Close() 281 | if n, err := s.Write([]byte(in)); n != 0 || err == nil { 282 | t.Fatalf("s.Write(%q) expected 0 (closed); got %v (%v)", in, n, err) 283 | } 284 | if n, err := c.Read(b); n != 0 || err != io.EOF { 285 | t.Fatalf("c.Read(b) expected 0 (EOF); got %v (%v)", n, err) 286 | } 287 | } 288 | 289 | func TestTransportBasic(t *testing.T) { 290 | c, s := newTestConn(1024) 291 | C, S := newTransport(c, nil), newTransport(s, nil) 292 | 293 | // Transport status 294 | if C.Compressed() { 295 | t.Error("C.Compressed() expected false") 296 | } 297 | if C.Encrypted() { 298 | t.Error("C.Encrypted() expected false") 299 | } 300 | if C.Closed() { 301 | t.Error("C.Closed() expected false") 302 | } 303 | 304 | // Write greeting 305 | in := "* IMAP4rev1 Server ready" 306 | if err := S.writeln(in); err != nil { 307 | t.Fatalf("S.writeln(%q) unexpected error; %v", in, err) 308 | } 309 | 310 | // Client doesn't receive anything until Flush is called 311 | select { 312 | case <-c.r: 313 | t.Fatal("<-c.r should have blocked") 314 | default: 315 | } 316 | 317 | // Flush greeting 318 | if err := S.Flush(); err != nil { 319 | t.Fatalf("S.Flush() unexpected error; %v", err) 320 | } 321 | 322 | // Receive greeting 323 | if out, err := C.readln(); out != in || err != nil { 324 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 325 | } 326 | 327 | tCAPABILITY(t, C, S, "A001") 328 | tLOGIN(t, C, S, "A002") 329 | tLOGOUT(t, C, S, "A003") 330 | } 331 | 332 | func TestTransportDeflate(t *testing.T) { 333 | c, s := newTestConn(1024) 334 | C, S := newTransport(c, nil), newTransport(s, nil) 335 | 336 | tGREETING(t, C, S) 337 | tCAPABILITY(t, C, S, "B001") 338 | tDEFLATE(t, C, S, "B002") 339 | tLOGIN(t, C, S, "B003") 340 | tLOGOUT(t, C, S, "B004") 341 | } 342 | 343 | func TestTransportTLS(t *testing.T) { 344 | c, s := newTestConn(1024) 345 | C, S := newTransport(c, nil), newTransport(s, nil) 346 | 347 | tGREETING(t, C, S) 348 | tCAPABILITY(t, C, S, "C001") 349 | tSTARTTLS(t, C, S, "C002") 350 | tLOGIN(t, C, S, "C003") 351 | tLOGOUT(t, C, S, "C004") 352 | } 353 | 354 | func TestTransportDeflateTLS(t *testing.T) { 355 | c, s := newTestConn(1024) 356 | C, S := newTransport(c, nil), newTransport(s, nil) 357 | 358 | tGREETING(t, C, S) 359 | tCAPABILITY(t, C, S, "D001") 360 | tDEFLATE(t, C, S, "D002") 361 | tSTARTTLS(t, C, S, "D003") 362 | tLOGIN(t, C, S, "D004") 363 | tLOGOUT(t, C, S, "D005") 364 | } 365 | 366 | func TestTransportTLSDeflate(t *testing.T) { 367 | c, s := newTestConn(1024) 368 | C, S := newTransport(c, nil), newTransport(s, nil) 369 | 370 | tGREETING(t, C, S) 371 | tSTARTTLS(t, C, S, "E001") 372 | tCAPABILITY(t, C, S, "E002") 373 | tDEFLATE(t, C, S, "E003") 374 | tLOGIN(t, C, S, "E004") 375 | tLOGOUT(t, C, S, "E005") 376 | } 377 | 378 | func TestTransportErrors(t *testing.T) { 379 | c, s := newTestConn(1024) 380 | 381 | // Client will use a 16-byte buffer 382 | orig := BufferSize 383 | BufferSize = 16 384 | C := newTransport(c, nil) 385 | BufferSize = orig 386 | S := newTransport(s, nil) 387 | 388 | // Line too long (write) 389 | in := "hello, world!!!" // 15 + 2 390 | if C.send(in) == nil { 391 | t.Fatalf("C.send(%q) expected error", in) 392 | } 393 | if out, err := S.readln(); out != "" || err == nil { 394 | t.Fatalf("S.readln() expected timeout; got %q (%v)", out, err) 395 | } 396 | 397 | // Line too long (before implicit flush) 398 | in = "hello, world" 399 | for i := 0; i < 2; i++ { 400 | if err := C.writeln(in); err != nil { 401 | t.Fatalf("C.writeln(%q) unexpected error; %v", in, err) 402 | } 403 | in = "!!!" // second writeln flushes the first 404 | } 405 | in = "hello, world" 406 | for i := 0; i < 2; i++ { 407 | if out, err := S.readln(); out != in || err != nil { 408 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 409 | } 410 | in = "!!!" 411 | C.Flush() 412 | } 413 | 414 | // Line too long (read) 415 | in = "hello, world!!!" 416 | if err := S.send(in); err != nil { 417 | t.Fatalf("S.send(%q) unexpected error; %v", in, err) 418 | } 419 | in += "\r" 420 | for i := 0; i < 2; i++ { 421 | if out, err := C.readln(); out != in || err == nil { 422 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 423 | } 424 | in = "\n" 425 | } 426 | 427 | // Bad input 428 | tests := []string{ 429 | "* hello\r", 430 | "* hello\n", 431 | "* hello\x00", 432 | "* hello\r\n", 433 | } 434 | for _, in := range tests { 435 | if S.writeln(in) == nil { 436 | t.Fatalf("S.writeln(%q) expected error", in) 437 | } 438 | } 439 | 440 | // Bad output 441 | tests = []string{ 442 | "* hello\n", 443 | "* hello\r\r\n", 444 | "* hello\x00\n", 445 | "* hello\x00\r\n", 446 | } 447 | for _, in := range tests { 448 | S.write(in) // use write to bypass line format checks in writeln 449 | } 450 | S.Flush() 451 | for _, in := range tests { 452 | if out, err := C.readln(); out != in || err == nil { 453 | t.Fatalf("C.readln() expected %q (bad line); got %q (%v)", in, out, err) 454 | } 455 | } 456 | } 457 | 458 | func tGREETING(t *testing.T, C, S *transport) { 459 | // Send greeting 460 | in := "* IMAP4rev1 Server ready" 461 | if err := S.send(in); err != nil { 462 | t.Fatalf("S.send(%q) unexpected error; %v", in, err) 463 | } 464 | 465 | // Receive greeting 466 | if out, err := C.readln(); out != in || err != nil { 467 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 468 | } 469 | } 470 | 471 | func tCAPABILITY(t *testing.T, C, S *transport, tag string) { 472 | // Send command 473 | in := tag + " CAPABILITY" 474 | if err := C.send(in); err != nil { 475 | t.Fatalf("C.send(%q) unexpected error; %v", in, err) 476 | } 477 | 478 | // Receive command 479 | if out, err := S.readln(); out != in || err != nil { 480 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 481 | } 482 | 483 | // Execute command 484 | rsp := []string{ 485 | "* CAPABILITY IMAP4rev1 STARTTLS COMPRESS=DEFLATE LITERAL+", 486 | tag + " OK CAPABILITY completed", 487 | } 488 | if err := S.send(rsp...); err != nil { 489 | t.Fatalf("S.send(rsp...) unexpected error; %v", err) 490 | } 491 | 492 | // Receive response 493 | for _, in := range rsp { 494 | if out, err := C.readln(); out != in || err != nil { 495 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 496 | } 497 | } 498 | } 499 | 500 | func tDEFLATE(t *testing.T, C, S *transport, tag string) { 501 | // Send command 502 | in := tag + " COMPRESS DEFLATE" 503 | if err := C.send(in); err != nil { 504 | t.Fatalf("C.send(%q) unexpected error; %v", in, err) 505 | } 506 | 507 | // Receive command 508 | if out, err := S.readln(); out != in || err != nil { 509 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 510 | } 511 | 512 | // Execute command 513 | in = tag + " OK DEFLATE active" 514 | if err := S.send(in); err != nil { 515 | t.Fatalf("S.send(%q) unexpected error; %v", in, err) 516 | } 517 | if err := S.EnableDeflate(0); err != nil { 518 | t.Fatalf("S.EnableDeflate(0) unexpected error; %v", err) 519 | } 520 | 521 | // Receive response 522 | if out, err := C.readln(); out != in || err != nil { 523 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 524 | } 525 | if err := C.EnableDeflate(9); err != nil { 526 | t.Fatalf("C.EnableDeflate(9) unexpected error; %v", err) 527 | } 528 | 529 | // Check status 530 | if !C.Compressed() { 531 | t.Error("C.Compressed() expected true") 532 | } 533 | if !S.Compressed() { 534 | t.Error("S.Compressed() expected true") 535 | } 536 | 537 | // Compression already enabled 538 | if err := C.EnableDeflate(6); err == nil { 539 | t.Fatal("C.EnableDeflate(6) expected error") 540 | } 541 | } 542 | 543 | func tSTARTTLS(t *testing.T, C, S *transport, tag string) { 544 | // Send command 545 | in := tag + " STARTTLS" 546 | if err := C.send(in); err != nil { 547 | t.Fatalf("C.send(%q) unexpected error; %v", in, err) 548 | } 549 | 550 | // Receive command 551 | if out, err := S.readln(); out != in || err != nil { 552 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 553 | } 554 | 555 | // Execute command 556 | in = tag + " OK Begin TLS negotiation now" 557 | if err := S.send(in); err != nil { 558 | t.Fatalf("S.send(%q) unexpected error; %v", in, err) 559 | } 560 | 561 | // Receive response 562 | if out, err := C.readln(); out != in || err != nil { 563 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 564 | } 565 | 566 | // Perform TLS negotiation 567 | result := make(chan error, 1) 568 | go func() { 569 | defer close(result) 570 | result <- S.starttls(false) 571 | }() 572 | if err := C.starttls(true); err != nil { 573 | t.Fatalf("C.EnableTLS() unexpected error; %v", err) 574 | } 575 | if err, ok := <-result; err != nil || ok != true { 576 | t.Fatalf("tls.Server.Handshake() unexpected error; %v", err) 577 | } 578 | 579 | // Check status 580 | if !C.Encrypted() { 581 | t.Error("C.Encrypted() expected true") 582 | } 583 | if !S.Encrypted() { 584 | t.Error("S.Encrypted() expected true") 585 | } 586 | 587 | // Encryption already enabled 588 | if err := C.EnableTLS(nil); err == nil { 589 | t.Fatal("C.EnableTLS(nil) expected error") 590 | } 591 | } 592 | 593 | func tLOGIN(t *testing.T, C, S *transport, tag string) { 594 | // Send command 595 | cmd := []string{ 596 | tag + " LOGIN {11+}", 597 | "FRED FOOBAR", 598 | " {7+}", 599 | "fat man", 600 | "", 601 | } 602 | if err := C.send(cmd...); err != nil { 603 | t.Fatalf("C.send(cmd...) unexpected error; %v", err) 604 | } 605 | 606 | // Receive command 607 | literal := false 608 | for _, in := range cmd { 609 | if literal { 610 | if out, err := S.read(len(in)); out != in || err != nil { 611 | t.Fatalf("S.read(%v) expected %q; got %q (%v)", len(in), in, out, err) 612 | } 613 | literal = false 614 | } else { 615 | if out, err := S.readln(); out != in || err != nil { 616 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 617 | } 618 | literal = len(in) > 0 && in[len(in)-1] == '}' 619 | } 620 | } 621 | 622 | // Execute command 623 | in := tag + " OK LOGIN completed" 624 | if err := S.send(in); err != nil { 625 | t.Fatalf("S.send(%q) unexpected error; %v", in, err) 626 | } 627 | 628 | // Receive response 629 | if out, err := C.readln(); out != in || err != nil { 630 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 631 | } 632 | } 633 | 634 | func tLOGOUT(t *testing.T, C, S *transport, tag string) { 635 | // Send command 636 | in := tag + " LOGOUT" 637 | if err := C.send(in); err != nil { 638 | t.Fatalf("C.send(%q) unexpected error; %v", in, err) 639 | } 640 | 641 | // Receive command 642 | if out, err := S.readln(); out != in || err != nil { 643 | t.Fatalf("S.readln() expected %q; got %q (%v)", in, out, err) 644 | } 645 | 646 | // Execute command 647 | rsp := []string{ 648 | "* BYE IMAP4rev1 Server logging out", 649 | tag + " OK LOGOUT completed", 650 | } 651 | for _, in := range rsp { 652 | if err := S.writeln(in); err != nil { 653 | t.Fatalf("S.writeln(%q) unexpected error; %v", in, err) 654 | } 655 | } 656 | if err := S.Close(true); err != nil { 657 | t.Fatalf("S.Close(true) unexpected error; %v", err) 658 | } 659 | 660 | // Receive response 661 | for _, in := range rsp { 662 | if out, err := C.readln(); out != in || err != nil { 663 | t.Fatalf("C.readln() expected %q; got %q (%v)", in, out, err) 664 | } 665 | } 666 | 667 | // Receive EOF and close 668 | if out, err := C.readln(); out != "" || err != io.EOF { 669 | t.Fatalf("C.readln() expected EOF; got %q (%v)", out, err) 670 | } 671 | if err := C.Close(false); err != nil { 672 | t.Fatalf("C.Close(false) unexpected error; %v", err) 673 | } 674 | 675 | // Check status 676 | if !C.Closed() { 677 | t.Error("C.Closed() expected true") 678 | } 679 | if !S.Closed() { 680 | t.Error("S.Closed() expected true") 681 | } 682 | } 683 | -------------------------------------------------------------------------------- /imap/utf7.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "encoding/base64" 9 | "errors" 10 | "unicode/utf16" 11 | "unicode/utf8" 12 | ) 13 | 14 | const ( 15 | uRepl = '\uFFFD' // Unicode replacement code point 16 | u7min = 0x20 // Minimum self-representing UTF-7 value 17 | u7max = 0x7E // Maximum self-representing UTF-7 value 18 | ) 19 | 20 | // ErrBadUTF7 is returned to indicate invalid modified UTF-7 encoding. 21 | var ErrBadUTF7 = errors.New("imap: bad utf-7 encoding") 22 | 23 | // Base64 codec for code points outside of the 0x20-0x7E range. 24 | var u7enc = base64.NewEncoding( 25 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,") 26 | 27 | // UTF7Encode converts a string from UTF-8 encoding to modified UTF-7. This 28 | // encoding is used by the Mailbox International Naming Convention (RFC 3501 29 | // section 5.1.3). Invalid UTF-8 byte sequences are replaced by the Unicode 30 | // replacement code point (U+FFFD). 31 | func UTF7Encode(s string) string { 32 | return string(UTF7EncodeBytes([]byte(s))) 33 | } 34 | 35 | // UTF7EncodeBytes converts a byte slice from UTF-8 encoding to modified UTF-7. 36 | func UTF7EncodeBytes(s []byte) []byte { 37 | u := make([]byte, 0, len(s)*2) 38 | for i, n := 0, len(s); i < n; { 39 | if c := s[i]; u7min <= c && c <= u7max { 40 | i++ 41 | if u = append(u, c); c == '&' { 42 | u = append(u, '-') 43 | } 44 | continue 45 | } 46 | start := i 47 | for i++; i < n && (s[i] < u7min || s[i] > u7max); i++ { 48 | // Find the next printable ASCII code point 49 | } 50 | u = append(u, utf7enc(s[start:i])...) 51 | } 52 | return u 53 | } 54 | 55 | // utf7enc converts string s from UTF-8 to UTF-16-BE, encodes the result as 56 | // Base64, removes the padding, and adds UTF-7 shifts. 57 | func utf7enc(s []byte) []byte { 58 | // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no 59 | // control code points (see table below). 60 | b := make([]byte, 0, len(s)+4) 61 | for len(s) > 0 { 62 | r, size := utf8.DecodeRune(s) 63 | if r > utf8.MaxRune { 64 | r, size = utf8.RuneError, 1 // Bug fix (issue 3785) 65 | } 66 | s = s[size:] 67 | if r1, r2 := utf16.EncodeRune(r); r1 != uRepl { 68 | b = append(b, byte(r1>>8), byte(r1)) 69 | r = r2 70 | } 71 | b = append(b, byte(r>>8), byte(r)) 72 | } 73 | 74 | // Encode as Base64 75 | n := u7enc.EncodedLen(len(b)) + 2 76 | b64 := make([]byte, n) 77 | u7enc.Encode(b64[1:], b) 78 | 79 | // Strip padding 80 | n -= 2 - (len(b)+2)%3 81 | b64 = b64[:n] 82 | 83 | // Add UTF-7 shifts 84 | b64[0] = '&' 85 | b64[n-1] = '-' 86 | return b64 87 | } 88 | 89 | // UTF7Decode converts a string from modified UTF-7 encoding to UTF-8. 90 | func UTF7Decode(u string) (s string, err error) { 91 | b, err := UTF7DecodeBytes([]byte(u)) 92 | s = string(b) 93 | return 94 | } 95 | 96 | // UTF7DecodeBytes converts a byte slice from modified UTF-7 encoding to UTF-8. 97 | func UTF7DecodeBytes(u []byte) (s []byte, err error) { 98 | s = make([]byte, 0, len(u)) 99 | ascii := true 100 | for i, n := 0, len(u); i < n; i++ { 101 | if c := u[i]; c < u7min || c > u7max { 102 | return nil, ErrBadUTF7 // Illegal code point in ASCII mode 103 | } else if c != '&' { 104 | s = append(s, c) 105 | ascii = true 106 | continue 107 | } 108 | start := i + 1 109 | // Find the end of the Base64 or "&-" segment 110 | for i++; i < n && u[i] != '-'; i++ { 111 | if u[i] == cr || u[i] == lf { // base64 package ignores CR and LF 112 | return nil, ErrBadUTF7 113 | } 114 | } 115 | if i == n { 116 | return nil, ErrBadUTF7 // Implicit shift ("&...") 117 | } else if i == start { 118 | s = append(s, '&') // Escape sequence "&-" 119 | ascii = true 120 | } else if b := utf7dec(u[start:i]); ascii && len(b) > 0 { 121 | s = append(s, b...) // Control or non-ASCII code points in Base64 122 | ascii = false 123 | } else { 124 | return nil, ErrBadUTF7 // Null shift ("&...-&...-") or bad encoding 125 | } 126 | } 127 | return 128 | } 129 | 130 | // utf7dec extracts UTF-16-BE bytes from Base64 data and converts them to UTF-8. 131 | // A nil slice is returned if the encoding is invalid. 132 | func utf7dec(b64 []byte) []byte { 133 | var b []byte 134 | 135 | // Allocate a single block of memory large enough to store the Base64 data 136 | // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. 137 | // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, 138 | // double the space allocation for UTF-8. 139 | if n := len(b64); b64[n-1] == '=' { 140 | return nil 141 | } else if n&3 == 0 { 142 | b = make([]byte, u7enc.DecodedLen(n)*3) 143 | } else { 144 | n += 4 - n&3 145 | b = make([]byte, n+u7enc.DecodedLen(n)*3) 146 | copy(b[copy(b, b64):n], []byte("==")) 147 | b64, b = b[:n], b[n:] 148 | } 149 | 150 | // Decode Base64 into the first 1/3rd of b 151 | n, err := u7enc.Decode(b, b64) 152 | if err != nil || n&1 == 1 { 153 | return nil 154 | } 155 | 156 | // Decode UTF-16-BE into the remaining 2/3rds of b 157 | b, s := b[:n], b[n:] 158 | j := 0 159 | for i := 0; i < n; i += 2 { 160 | r := rune(b[i])<<8 | rune(b[i+1]) 161 | if utf16.IsSurrogate(r) { 162 | if i += 2; i == n { 163 | return nil 164 | } 165 | r2 := rune(b[i])<<8 | rune(b[i+1]) 166 | if r = utf16.DecodeRune(r, r2); r == uRepl { 167 | return nil 168 | } 169 | } else if u7min <= r && r <= u7max { 170 | return nil 171 | } 172 | j += utf8.EncodeRune(s[j:], r) 173 | } 174 | return s[:j] 175 | } 176 | 177 | /* 178 | The following table shows the number of bytes required to encode each code point 179 | in the specified range using UTF-8 and UTF-16 representations: 180 | 181 | +-----------------+-------+--------+ 182 | | Code points | UTF-8 | UTF-16 | 183 | +-----------------+-------+--------+ 184 | | 000000 - 00007F | 1 | 2 | 185 | | 000080 - 0007FF | 2 | 2 | 186 | | 000800 - 00FFFF | 3 | 2 | 187 | | 010000 - 10FFFF | 4 | 4 | 188 | +-----------------+-------+--------+ 189 | 190 | Source: http://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings 191 | */ 192 | -------------------------------------------------------------------------------- /imap/utf7_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import "testing" 8 | 9 | var encode = []struct { 10 | in string 11 | out string 12 | ok bool 13 | }{ 14 | // Printable ASCII 15 | {"", "", true}, 16 | {"a", "a", true}, 17 | {"ab", "ab", true}, 18 | {"-", "-", true}, 19 | {"&", "&-", true}, 20 | {"&&", "&-&-", true}, 21 | {"&&&-&", "&-&-&--&-", true}, 22 | {"-&*&-", "-&-*&--", true}, 23 | {"a&b", "a&-b", true}, 24 | {"a&", "a&-", true}, 25 | {"&b", "&-b", true}, 26 | {"-a&", "-a&-", true}, 27 | {"&b-", "&-b-", true}, 28 | 29 | // Unicode range 30 | {"\u0000", "&AAA-", true}, 31 | {"\n", "&AAo-", true}, 32 | {"\r", "&AA0-", true}, 33 | {"\u001F", "&AB8-", true}, 34 | {"\u0020", " ", true}, 35 | {"\u0025", "%", true}, 36 | {"\u0026", "&-", true}, 37 | {"\u0027", "'", true}, 38 | {"\u007E", "~", true}, 39 | {"\u007F", "&AH8-", true}, 40 | {"\u0080", "&AIA-", true}, 41 | {"\u00FF", "&AP8-", true}, 42 | {"\u07FF", "&B,8-", true}, 43 | {"\u0800", "&CAA-", true}, 44 | {"\uFFEF", "&,+8-", true}, 45 | {"\uFFFF", "&,,8-", true}, 46 | {"\U00010000", "&2ADcAA-", true}, 47 | {"\U0010FFFF", "&2,,f,w-", true}, 48 | 49 | // Padding 50 | {"\x00\x1F", "&AAAAHw-", true}, // 2 51 | {"\x00\x1F\x7F", "&AAAAHwB,-", true}, // 0 52 | {"\x00\x1F\x7F\u0080", "&AAAAHwB,AIA-", true}, // 1 53 | {"\x00\x1F\x7F\u0080\u00FF", "&AAAAHwB,AIAA,w-", true}, // 2 54 | 55 | // Mix 56 | {"a\x00", "a&AAA-", true}, 57 | {"\x00a", "&AAA-a", true}, 58 | {"&\x00", "&-&AAA-", true}, 59 | {"\x00&", "&AAA-&-", true}, 60 | {"a\x00&", "a&AAA-&-", true}, 61 | {"a&\x00", "a&-&AAA-", true}, 62 | {"&a\x00", "&-a&AAA-", true}, 63 | {"&\x00a", "&-&AAA-a", true}, 64 | {"\x00&a", "&AAA-&-a", true}, 65 | {"\x00a&", "&AAA-a&-", true}, 66 | {"ab&\uFFFF", "ab&-&,,8-", true}, 67 | {"a&b\uFFFF", "a&-b&,,8-", true}, 68 | {"&ab\uFFFF", "&-ab&,,8-", true}, 69 | {"ab\uFFFF&", "ab&,,8-&-", true}, 70 | {"a\uFFFFb&", "a&,,8-b&-", true}, 71 | {"\uFFFFab&", "&,,8-ab&-", true}, 72 | 73 | {"\x20\x25&\x27\x7E", " %&-'~", true}, 74 | {"\x1F\x20&\x7E\x7F", "&AB8- &-~&AH8-", true}, 75 | {"&\x00\x19\x7F\u0080", "&-&AAAAGQB,AIA-", true}, 76 | {"\x00&\x19\x7F\u0080", "&AAA-&-&ABkAfwCA-", true}, 77 | {"\x00\x19&\x7F\u0080", "&AAAAGQ-&-&AH8AgA-", true}, 78 | {"\x00\x19\x7F&\u0080", "&AAAAGQB,-&-&AIA-", true}, 79 | {"\x00\x19\x7F\u0080&", "&AAAAGQB,AIA-&-", true}, 80 | {"&\x00\x1F\x7F\u0080", "&-&AAAAHwB,AIA-", true}, 81 | {"\x00&\x1F\x7F\u0080", "&AAA-&-&AB8AfwCA-", true}, 82 | {"\x00\x1F&\x7F\u0080", "&AAAAHw-&-&AH8AgA-", true}, 83 | {"\x00\x1F\x7F&\u0080", "&AAAAHwB,-&-&AIA-", true}, 84 | {"\x00\x1F\x7F\u0080&", "&AAAAHwB,AIA-&-", true}, 85 | 86 | // Russian 87 | {"\u041C\u0430\u043A\u0441\u0438\u043C \u0425\u0438\u0442\u0440\u043E\u0432", 88 | "&BBwEMAQ6BEEEOAQ8- &BCUEOARCBEAEPgQy-", true}, 89 | 90 | // RFC 3501 91 | {"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true}, 92 | {"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true}, 93 | {"\u263A!", "&Jjo-!", true}, 94 | {"\u53F0\u5317\u65E5\u672C\u8A9E", "&U,BTF2XlZyyKng-", true}, 95 | 96 | // RFC 2152 (modified) 97 | {"\u0041\u2262\u0391\u002E", "A&ImIDkQ-.", true}, 98 | {"Hi Mom -\u263A-!", "Hi Mom -&Jjo--!", true}, 99 | {"\u65E5\u672C\u8A9E", "&ZeVnLIqe-", true}, 100 | 101 | // 8->16 and 24->16 byte UTF-8 to UTF-16 conversion 102 | {"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007", "&AAAAAQACAAMABAAFAAYABw-", true}, 103 | {"\u0800\u0801\u0802\u0803\u0804\u0805\u0806\u0807", "&CAAIAQgCCAMIBAgFCAYIBw-", true}, 104 | 105 | // Invalid UTF-8 (bad bytes are converted to U+FFFD) 106 | {"\xC0\x80", "&,,3,,Q-", false}, // U+0000 107 | {"\xF4\x90\x80\x80", "&,,3,,f,9,,0-", false}, // U+110000 108 | {"\xF7\xBF\xBF\xBF", "&,,3,,f,9,,0-", false}, // U+1FFFFF 109 | {"\xF8\x88\x80\x80\x80", "&,,3,,f,9,,3,,Q-", false}, // U+200000 110 | {"\xF4\x8F\xBF\x3F", "&,,3,,f,9-?", false}, // U+10FFFF (bad byte) 111 | {"\xF4\x8F\xBF", "&,,3,,f,9-", false}, // U+10FFFF (short) 112 | {"\xF4\x8F", "&,,3,,Q-", false}, 113 | {"\xF4", "&,,0-", false}, 114 | {"\x00\xF4\x00", "&AAD,,QAA-", false}, 115 | } 116 | 117 | var decode = []struct { 118 | in string 119 | out string 120 | ok bool 121 | }{ 122 | // Basics (the inverse test on encode checks other valid inputs) 123 | {"", "", true}, 124 | {"abc", "abc", true}, 125 | {"&-abc", "&abc", true}, 126 | {"abc&-", "abc&", true}, 127 | {"a&-b&-c", "a&b&c", true}, 128 | {"&ABk-", "\x19", true}, 129 | {"&AB8-", "\x1F", true}, 130 | {"ABk-", "ABk-", true}, 131 | {"&-,&-&AP8-&-", "&,&\u00FF&", true}, 132 | {"&-&-,&AP8-&-", "&&,\u00FF&", true}, 133 | {"abc &- &AP8A,wD,- &- xyz", "abc & \u00FF\u00FF\u00FF & xyz", true}, 134 | 135 | // Illegal code point in ASCII 136 | {"\x00", "", false}, 137 | {"\x1F", "", false}, 138 | {"abc\n", "", false}, 139 | {"abc\x7Fxyz", "", false}, 140 | {"\uFFFD", "", false}, 141 | {"\u041C", "", false}, 142 | 143 | // Invalid Base64 alphabet 144 | {"&/+8-", "", false}, 145 | {"&*-", "", false}, 146 | {"&ZeVnLIqe -", "", false}, 147 | 148 | // CR and LF in Base64 149 | {"&ZeVnLIqe\r\n-", "", false}, 150 | {"&ZeVnLIqe\r\n\r\n-", "", false}, 151 | {"&ZeVn\r\n\r\nLIqe-", "", false}, 152 | 153 | // Padding not stripped 154 | {"&AAAAHw=-", "", false}, 155 | {"&AAAAHw==-", "", false}, 156 | {"&AAAAHwB,AIA=-", "", false}, 157 | {"&AAAAHwB,AIA==-", "", false}, 158 | 159 | // One byte short 160 | {"&2A-", "", false}, 161 | {"&2ADc-", "", false}, 162 | {"&AAAAHwB,A-", "", false}, 163 | {"&AAAAHwB,A=-", "", false}, 164 | {"&AAAAHwB,A==-", "", false}, 165 | {"&AAAAHwB,A===-", "", false}, 166 | {"&AAAAHwB,AI-", "", false}, 167 | {"&AAAAHwB,AI=-", "", false}, 168 | {"&AAAAHwB,AI==-", "", false}, 169 | 170 | // Implicit shift 171 | {"&", "", false}, 172 | {"&Jjo", "", false}, 173 | {"Jjo&", "", false}, 174 | {"&Jjo&", "", false}, 175 | {"&Jjo!", "", false}, 176 | {"&Jjo+", "", false}, 177 | {"abc&Jjo", "", false}, 178 | 179 | // Null shift 180 | {"&AGE-&Jjo-", "", false}, 181 | {"&U,BTFw-&ZeVnLIqe-", "", false}, 182 | 183 | // ASCII in Base64 184 | {"&AGE-", "", false}, // "a" 185 | {"&ACY-", "", false}, // "&" 186 | {"&AGgAZQBsAGwAbw-", "", false}, // "hello" 187 | {"&JjoAIQ-", "", false}, // "\u263a!" 188 | 189 | // Bad surrogate 190 | {"&2AA-", "", false}, // U+D800 191 | {"&2AD-", "", false}, // U+D800 192 | {"&3AA-", "", false}, // U+DC00 193 | {"&2AAAQQ-", "", false}, // U+D800 'A' 194 | {"&2AD,,w-", "", false}, // U+D800 U+FFFF 195 | {"&3ADYAA-", "", false}, // U+DC00 U+D800 196 | } 197 | 198 | func TestUTF7Encode(t *testing.T) { 199 | for _, test := range encode { 200 | out := UTF7Encode(test.in) 201 | if out != test.out { 202 | t.Errorf("UTF7Encode(%+q) expected %+q; got %+q", test.in, test.out, out) 203 | } 204 | } 205 | } 206 | 207 | func TestUTF7Decode(t *testing.T) { 208 | for _, test := range decode { 209 | out, err := UTF7Decode(test.in) 210 | if out != test.out { 211 | t.Errorf("UTF7Decode(%+q) expected %+q; got %+q", test.in, test.out, out) 212 | } 213 | if test.ok { 214 | if err != nil { 215 | t.Errorf("UTF7Decode(%+q) unexpected error; %v", test.in, err) 216 | } 217 | } else if err == nil { 218 | t.Errorf("UTF7Decode(%+q) expected error", test.in) 219 | } 220 | } 221 | } 222 | 223 | func TestUTF7Inverse(t *testing.T) { 224 | for _, test := range encode { 225 | if test.ok { 226 | in, _ := UTF7Decode(UTF7Encode(test.in)) 227 | if in != test.in { 228 | t.Errorf("EncodeInverse(%+q) expected %+q; got %+q", test.in, test.in, in) 229 | } 230 | } 231 | } 232 | for _, test := range decode { 233 | if test.ok { 234 | out, _ := UTF7Decode(test.in) 235 | in := UTF7Encode(out) 236 | if in != test.in { 237 | t.Errorf("DecodeInverse(%+q) expected %+q; got %+q", test.in, test.in, in) 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /imap/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package imap 6 | 7 | import ( 8 | "crypto/tls" 9 | "encoding/base64" 10 | "fmt" 11 | "log" 12 | "math/rand" 13 | "net" 14 | "os" 15 | "strconv" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | // Default debug logging configuration for new Client instances. 21 | var ( 22 | DefaultLogger = log.New(os.Stderr, "[imap] ", log.Ltime) 23 | DefaultLogMask = LogNone 24 | ) 25 | 26 | // prng is a deterministic pseudo-random number generator seeded using the 27 | // system clock. 28 | var prng = rand.New(&prngSource{src: rand.NewSource(time.Now().UnixNano())}) 29 | 30 | // gotest is set to true when the package is being executed by the test command. 31 | // It causes the Client to use predictable tag ids for scripting. 32 | var gotest = false 33 | 34 | // debugLog handles all logging operations for Client and transport. 35 | type debugLog struct { 36 | log *log.Logger // Message destination 37 | mask LogMask // Enabled message categories 38 | } 39 | 40 | // newDebugLog returns a new debugLog instance. 41 | func newDebugLog(log *log.Logger, mask LogMask) *debugLog { 42 | if log == nil { 43 | log = DefaultLogger 44 | } 45 | return &debugLog{log, mask} 46 | } 47 | 48 | // SetLogger sets the destination of debug messages and returns the previous 49 | // logger. 50 | func (d *debugLog) SetLogger(log *log.Logger) *log.Logger { 51 | if d == nil { 52 | return nil 53 | } 54 | prev := d.log 55 | d.log = log 56 | return prev 57 | } 58 | 59 | // SetLogMask enables/disables debug message categories and returns the previous 60 | // mask. 61 | func (d *debugLog) SetLogMask(mask LogMask) LogMask { 62 | if d == nil { 63 | return LogNone 64 | } 65 | prev := d.mask 66 | d.mask = mask 67 | return prev 68 | } 69 | 70 | // Log formats its arguments using default formatting, analogous to Print(), and 71 | // records the text in the debug log if logging is enabled for the specified 72 | // mask. 73 | func (d *debugLog) Log(mask LogMask, v ...interface{}) { 74 | if d != nil && d.mask&mask == mask { 75 | d.log.Output(2, fmt.Sprint(v...)) 76 | } 77 | } 78 | 79 | // Logf formats its arguments according to the format, analogous to Printf(), 80 | // and records the text in the debug log if logging is enabled for the specified 81 | // mask. 82 | func (d *debugLog) Logf(mask LogMask, format string, v ...interface{}) { 83 | if d != nil && d.mask&mask == mask { 84 | d.log.Output(2, fmt.Sprintf(format, v...)) 85 | } 86 | } 87 | 88 | // Logln formats its arguments using default formatting, analogous to Println(), 89 | // and records the text in the debug log if logging is enabled for the specified 90 | // mask. 91 | func (d *debugLog) Logln(mask LogMask, v ...interface{}) { 92 | if d != nil && d.mask&mask == mask { 93 | d.log.Output(2, fmt.Sprintln(v...)) 94 | } 95 | } 96 | 97 | // randStr returns a pseudo-random string of n upper case ASCII letters. 98 | func randStr(n int) string { 99 | s := make([]byte, n) 100 | if gotest { 101 | for i := range s { 102 | s[i] = 'A' 103 | } 104 | } else { 105 | for i := range s { 106 | s[i] = 'A' + byte(prng.Intn(26)) 107 | } 108 | } 109 | return string(s) 110 | } 111 | 112 | // tagGen is used to create unique command tags. 113 | type tagGen struct { 114 | id []byte 115 | seq uint64 116 | } 117 | 118 | // newTagGen returns a new tagGen instance with a random tag id consisting of n 119 | // unique upper case ASCII letters. 120 | func newTagGen(n int) *tagGen { 121 | if gotest { 122 | n = 1 123 | } else if n < 1 || 26 < n { 124 | n = 5 125 | } 126 | id := make([]byte, n, n+20) 127 | if gotest { 128 | id[0] = 'A' 129 | } else { 130 | for i, v := range prng.Perm(26)[:n] { 131 | id[i] = 'A' + byte(v) 132 | } 133 | } 134 | return &tagGen{id, 0} 135 | } 136 | 137 | // Next returns the tag that should be used by the next command. 138 | func (t *tagGen) Next() string { 139 | t.seq++ 140 | return string(strconv.AppendUint(t.id, t.seq, 10)) 141 | } 142 | 143 | // defaultPort joins addr and port if addr contains just the host name or IP. 144 | func defaultPort(addr, port string) string { 145 | _, _, err := net.SplitHostPort(addr) 146 | if err != nil { 147 | addr = net.JoinHostPort(addr, port) 148 | } 149 | return addr 150 | } 151 | 152 | // setServerName returns a new TLS configuration with ServerName set to host if 153 | // the original configuration was nil or config.ServerName was empty. 154 | func setServerName(config *tls.Config, host string) *tls.Config { 155 | if config == nil { 156 | config = &tls.Config{ServerName: host} 157 | } else if config.ServerName == "" { 158 | c := *config 159 | c.ServerName = host 160 | config = &c 161 | } 162 | return config 163 | } 164 | 165 | var b64codec = base64.StdEncoding 166 | 167 | // b64enc encodes src to Base64 representation, returning the result as a new 168 | // byte slice. 169 | func b64enc(src []byte) []byte { 170 | if src == nil { 171 | return nil 172 | } 173 | dst := make([]byte, b64codec.EncodedLen(len(src))) 174 | b64codec.Encode(dst, src) 175 | return dst 176 | } 177 | 178 | // b64dec decodes src from Base64 representation, returning the result as a new 179 | // byte slice. 180 | func b64dec(src []byte) ([]byte, error) { 181 | if src == nil { 182 | return nil, nil 183 | } 184 | dst := make([]byte, b64codec.DecodedLen(len(src))) 185 | n, err := b64codec.Decode(dst, src) 186 | return dst[:n], err 187 | } 188 | 189 | // prngSource is a goroutine-safe implementation of rand.Source. 190 | type prngSource struct { 191 | mu sync.Mutex 192 | src rand.Source 193 | } 194 | 195 | func (r *prngSource) Int63() (n int64) { 196 | r.mu.Lock() 197 | n = r.src.Int63() 198 | r.mu.Unlock() 199 | return 200 | } 201 | 202 | func (r *prngSource) Seed(seed int64) { 203 | r.mu.Lock() 204 | r.src.Seed(seed) 205 | r.mu.Unlock() 206 | } 207 | -------------------------------------------------------------------------------- /mock/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package mock implements a scripted IMAP server for testing client behavior. 7 | 8 | The mock server understands low-level details of the IMAP protocol (lines, 9 | literals, compression, encryption, etc.). It doesn't know anything about 10 | commands, users, mailboxes, messages, or any other high-level concepts. The 11 | server follows a script that tells it what to send/receive and when. Everything 12 | received from the client is checked against the script and an error is returned 13 | if there is a mismatch. 14 | 15 | See mock_test.go for examples of how to use this package in your unit tests. 16 | */ 17 | package mock 18 | 19 | import ( 20 | "crypto/tls" 21 | "fmt" 22 | "io" 23 | "net" 24 | "runtime" 25 | "strings" 26 | "testing" 27 | "time" 28 | 29 | "github.com/mxk/go-imap/imap" 30 | ) 31 | 32 | // ServerName is the hostname used by the scripted server. 33 | var ServerName = "imap.mock.net" 34 | 35 | // Timeout is the maximum execution time for each Read and Write call on the 36 | // simulated network connection. When the client or server aborts execution due 37 | // to an unexpected error, this timeout prevents the other side from blocking 38 | // indefinitely. 39 | var Timeout = 500 * time.Millisecond 40 | 41 | // Send and Recv are script actions for sending and receiving raw bytes (usually 42 | // literal strings). 43 | type ( 44 | Send []byte 45 | Recv []byte 46 | ) 47 | 48 | // ScriptFunc is function type called during script execution to control the 49 | // server state. STARTTLS, DEFLATE, and CLOSE are predefined script actions for 50 | // the most common operations. 51 | type ScriptFunc func(s imap.MockServer) error 52 | 53 | // Predefined script actions for controlling server state. 54 | var ( 55 | STARTTLS = func(s imap.MockServer) error { return s.EnableTLS(serverTLS()) } 56 | DEFLATE = func(s imap.MockServer) error { return s.EnableDeflate(-1) } 57 | CLOSE = func(s imap.MockServer) error { return s.Close(true) } 58 | ) 59 | 60 | // T wraps existing test state and provides methods for testing the IMAP client 61 | // against the scripted server. 62 | type T struct { 63 | *testing.T 64 | 65 | s imap.MockServer // Server instance 66 | ch <-chan interface{} // Script result channel 67 | 68 | c *imap.Client // Client instance 69 | cn net.Conn // Client connection used by Dial and DialTLS 70 | } 71 | 72 | // Server launches a new scripted server that can handle one client connection. 73 | // The script should contain the initial server greeting. It should also use the 74 | // STARTTLS action (or a custom ScriptFunc for negotiating encryption) prior to 75 | // sending the greeting if the client is using DialTLS to connect. 76 | func Server(t *testing.T, script ...interface{}) *T { 77 | c, s := NewConn("client", "server", 0) 78 | c.SetTimeout(Timeout) 79 | s.SetTimeout(Timeout) 80 | mt := &T{T: t, s: imap.NewMockServer(s), cn: c} 81 | mt.Script(script...) 82 | return mt 83 | } 84 | 85 | // Dial returns a new Client connected to the scripted server or an error if the 86 | // connection could not be established. 87 | func (t *T) Dial() (*imap.Client, error) { 88 | cn := t.cn 89 | if t.cn = nil; cn == nil { 90 | t.Fatalf(cl("t.Dial() or t.DialTLS() already called for this server")) 91 | } 92 | var err error 93 | if t.c, err = imap.NewClient(cn, ServerName, Timeout); err != nil { 94 | cn.Close() 95 | } 96 | return t.c, err 97 | } 98 | 99 | // DialTLS returns a new Client connected to the scripted server or an error if 100 | // the connection could not be established. The server is expected to negotiate 101 | // encryption before sending the initial greeting. Config should be nil when 102 | // used in combination with the predefined STARTTLS script action. 103 | func (t *T) DialTLS(config *tls.Config) (*imap.Client, error) { 104 | cn := t.cn 105 | if t.cn = nil; cn == nil { 106 | t.Fatalf(cl("t.Dial() or t.DialTLS() already called for this server")) 107 | } 108 | if config == nil { 109 | config = clientTLS() 110 | } 111 | tlsConn := tls.Client(cn, config) 112 | var err error 113 | if t.c, err = imap.NewClient(tlsConn, ServerName, Timeout); err != nil { 114 | cn.Close() 115 | } 116 | return t.c, err 117 | } 118 | 119 | // Script runs a server script in a separate goroutine. A script is a sequence 120 | // of string, Send, Recv, and ScriptFunc actions. Strings represent lines of 121 | // text to be sent ("S: ...") or received ("C: ...") by the server. There is an 122 | // implicit CRLF at the end of each line. Send and Recv allow the server to send 123 | // and receive raw bytes (usually literal strings). ScriptFunc allows server 124 | // state changes by calling methods on the provided imap.MockServer instance. 125 | func (t *T) Script(script ...interface{}) { 126 | select { 127 | case <-t.ch: 128 | default: 129 | if t.ch != nil { 130 | t.Fatalf(cl("t.Script() called while another script is active")) 131 | } 132 | } 133 | ch := make(chan interface{}, 1) 134 | t.ch = ch 135 | go t.script(script, ch) 136 | } 137 | 138 | // Join waits for script completion and reports any errors encountered by the 139 | // client or the server. 140 | func (t *T) Join(err error) { 141 | if err, ok := <-t.ch; err != nil { 142 | t.Errorf(cl("t.Join() S: %v"), err) 143 | } else if !ok { 144 | t.Errorf(cl("t.Join() called without an active script")) 145 | } 146 | if err != nil { 147 | t.Fatalf(cl("t.Join() C: %v"), err) 148 | } else if t.Failed() { 149 | t.FailNow() 150 | } 151 | } 152 | 153 | // StartTLS performs client-side TLS negotiation. Config should be nil when used 154 | // in combination with the predefined STARTTLS script action. 155 | func (t *T) StartTLS(config *tls.Config) error { 156 | if t.c == nil { 157 | t.Fatalf(cl("t.StartTLS() called without a valid client")) 158 | } 159 | if config == nil { 160 | config = clientTLS() 161 | } 162 | _, err := t.c.StartTLS(config) 163 | return err 164 | } 165 | 166 | // script runs the provided script and sends the first encountered error to ch, 167 | // which is then closed. 168 | func (t *T) script(script []interface{}, ch chan<- interface{}) { 169 | defer func() { ch <- recover(); close(ch) }() 170 | for ln, v := range script { 171 | switch ln++; v := v.(type) { 172 | case string: 173 | if strings.HasPrefix(v, "S: ") { 174 | err := t.s.WriteLine([]byte(v[3:])) 175 | t.flush(ln, v, err) 176 | } else if strings.HasPrefix(v, "C: ") { 177 | b, err := t.s.ReadLine() 178 | t.compare(ln, v[3:], string(b), err) 179 | } else { 180 | panicf(`[#%d] %+q must be prefixed with "S: " or "C: "`, ln, v) 181 | } 182 | case Send: 183 | _, err := t.s.Write(v) 184 | t.flush(ln, v, err) 185 | case Recv: 186 | b := make([]byte, len(v)) 187 | _, err := io.ReadFull(t.s, b) 188 | t.compare(ln, string(v), string(b), err) 189 | case ScriptFunc: 190 | t.run(ln, v) 191 | case func(s imap.MockServer) error: 192 | t.run(ln, v) 193 | default: 194 | panicf("[#%d] %T is not a valid script action", ln, v) 195 | } 196 | } 197 | } 198 | 199 | // flush sends any buffered data to the client and panics if there is an error. 200 | func (t *T) flush(ln int, v interface{}, err error) { 201 | if err == nil { 202 | err = t.s.Flush() 203 | } 204 | if err != nil { 205 | panicf("[#%d] %+q write error: %v", ln, v, err) 206 | } 207 | } 208 | 209 | // compare panics if v != b or err != nil. 210 | func (t *T) compare(ln int, v, b string, err error) { 211 | if v != b || err != nil { 212 | panicf("[#%d] expected %+q; got %+q (%v)", ln, v, b, err) 213 | } 214 | } 215 | 216 | // run calls v and panics if it returns an error. 217 | func (t *T) run(ln int, v ScriptFunc) { 218 | if err := v(t.s); err != nil { 219 | panicf("[#%d] ScriptFunc error: %v", ln, err) 220 | } 221 | } 222 | 223 | // cl prefixes s with the current line number in the calling test function. 224 | func cl(s string) string { 225 | _, testFile, line, ok := runtime.Caller(2) 226 | if ok && strings.HasSuffix(testFile, "_test.go") { 227 | return fmt.Sprintf("%d: %s", line, s) 228 | } 229 | return s 230 | } 231 | 232 | // panicf must be documented for consistency (you're welcome)! 233 | func panicf(format string, v ...interface{}) { 234 | panic(fmt.Sprintf(format, v...)) 235 | } 236 | -------------------------------------------------------------------------------- /mock/mock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mock_test 6 | 7 | import ( 8 | "io" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/mxk/go-imap/imap" 13 | "github.com/mxk/go-imap/mock" 14 | ) 15 | 16 | func init() { 17 | imap.DefaultLogMask = imap.LogRaw 18 | } 19 | 20 | func TestGreeting(T *testing.T) { 21 | // Typical greeting followed by the CAPABILITY command 22 | t := mock.Server(T, 23 | `S: * OK Server ready`, 24 | `C: A1 CAPABILITY`, 25 | `S: * CAPABILITY IMAP4rev1`, 26 | `S: A1 OK Thats all she wrote!`, 27 | ) 28 | _, err := t.Dial() 29 | t.Join(err) 30 | 31 | // Capabilities sent in the greeting 32 | t = mock.Server(T, 33 | `S: * OK [CAPABILITY IMAP4rev1] Server ready`, 34 | ) 35 | _, err = t.Dial() 36 | t.Join(err) 37 | 38 | // TLS negotiated before the greeting 39 | t = mock.Server(T, 40 | mock.STARTTLS, 41 | `S: * PREAUTH [CAPABILITY IMAP4rev1] Server ready`, 42 | ) 43 | _, err = t.DialTLS(nil) 44 | t.Join(err) 45 | 46 | // Connection refused 47 | t = mock.Server(T, 48 | `S: * BYE Server not ready`, 49 | mock.CLOSE, 50 | ) 51 | if _, err = t.Dial(); err == nil { 52 | t.Errorf("t.Dial() expected an error") 53 | } 54 | t.Join(nil) 55 | } 56 | 57 | func TestSession(T *testing.T) { 58 | t := mock.Server(T, 59 | `S: * OK [CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED] Server ready`, 60 | ) 61 | c, err := t.Dial() 62 | t.Join(err) 63 | 64 | // STARTTLS 65 | t.Script( 66 | `C: A1 STARTTLS`, 67 | `S: A1 OK Begin TLS negotiation now`, 68 | mock.STARTTLS, 69 | `C: A2 CAPABILITY`, 70 | `S: * CAPABILITY IMAP4rev1`, 71 | `S: A2 OK Thats all she wrote!`, 72 | ) 73 | t.Join(t.StartTLS(nil)) 74 | 75 | // LOGIN 76 | t.Script( 77 | `C: A3 LOGIN "joe" "password"`, 78 | `S: A3 OK LOGIN completed`, 79 | `C: A4 CAPABILITY`, 80 | `S: * CAPABILITY IMAP4rev1 COMPRESS=DEFLATE`, 81 | `S: A4 OK Thats all she wrote!`, 82 | ) 83 | _, err = c.Login("joe", "password") 84 | t.Join(err) 85 | 86 | // COMPRESS 87 | t.Script( 88 | `C: A5 COMPRESS DEFLATE`, 89 | `S: A5 OK DEFLATE active`, 90 | mock.DEFLATE, 91 | ) 92 | _, err = c.CompressDeflate(-1) 93 | t.Join(err) 94 | 95 | // LOGOUT 96 | t.Script( 97 | `C: A6 LOGOUT`, 98 | `S: * BYE LOGOUT Requested`, 99 | `S: A6 OK Quoth the raven, nevermore...`, 100 | mock.CLOSE, 101 | ) 102 | _, err = c.Logout(mock.Timeout) 103 | t.Join(err) 104 | 105 | // Verify EOF 106 | if err = c.Recv(mock.Timeout); err != io.EOF { 107 | t.Fatalf("c.Recv() expected EOF; got %v", err) 108 | } 109 | } 110 | 111 | func TestLiteral(T *testing.T) { 112 | t := mock.Server(T, 113 | `S: * PREAUTH [CAPABILITY IMAP4rev1] Server ready`, 114 | ) 115 | c, err := t.Dial() 116 | t.Join(err) 117 | 118 | flags := imap.NewFlagSet(`\Seen`) 119 | lines := []string{ 120 | "Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)", 121 | "From: Fred Foobar ", 122 | "Subject: afternoon meeting", 123 | "To: mooch@owatagu.siam.edu", 124 | "Message-Id: ", 125 | "MIME-Version: 1.0", 126 | "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII", 127 | "", 128 | "Hello Joe, do you think we can meet at 3:30 tomorrow?", 129 | "", 130 | } 131 | msg := []byte(strings.Join(lines, "\r\n")) 132 | lit := imap.NewLiteral(msg) 133 | 134 | // Embedded literal 135 | t.Script( 136 | `C: A1 APPEND "saved-messages" (\Seen) {310}`, 137 | `S: + Ready for literal data`, 138 | `C: `+lines[0], 139 | `C: `+lines[1], 140 | `C: `+lines[2], 141 | `C: `+lines[3], 142 | `C: `+lines[4], 143 | `C: `+lines[5], 144 | `C: `+lines[6], 145 | `C: `+lines[7], 146 | `C: `+lines[8], 147 | `C: `+lines[9], 148 | `S: A1 OK APPEND completed`, 149 | ) 150 | _, err = imap.Wait(c.Append("saved-messages", flags, nil, lit)) 151 | t.Join(err) 152 | 153 | // Recv action literal 154 | t.Script( 155 | `C: A2 APPEND "saved-messages" (\Seen) {310}`, 156 | `S: + Ready for literal data`, 157 | mock.Recv(msg), 158 | `C: `, 159 | `S: A2 OK APPEND completed`, 160 | ) 161 | _, err = imap.Wait(c.Append("saved-messages", flags, nil, lit)) 162 | t.Join(err) 163 | 164 | // Embedded and Send action literals from the server 165 | t.Script( 166 | `C: A3 LIST "" "*"`, 167 | `S: * LIST (\Noselect) "/" {3}`, 168 | `S: foo`, 169 | `S: * LIST () "/" {7}`, 170 | mock.Send("foo/bar"), 171 | `S: `, 172 | `S: A3 OK LIST completed`, 173 | ) 174 | _, err = imap.Wait(c.List("", "*")) 175 | t.Join(err) 176 | } 177 | -------------------------------------------------------------------------------- /mock/net.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mock 6 | 7 | import ( 8 | "io" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Conn is an in-memory implementation of net.Conn. 15 | type Conn struct { 16 | mu *sync.Mutex // Control mutex 17 | r *halfConn // Local side 18 | w *halfConn // Remote side 19 | rd time.Time // Read deadline 20 | wd time.Time // Write deadline 21 | t time.Duration // Read/write timeout 22 | } 23 | 24 | // NewConn creates a pair of connected net.Conn instances. The addresses are 25 | // arbitrary strings used to distinguish the two ends of the connection. bufSize 26 | // is the maximum number of bytes that can be written to each connection before 27 | // Write will block. A value <= 0 for bufSize means use a default of 4096 bytes. 28 | func NewConn(addrA, addrB string, bufSize int) (A *Conn, B *Conn) { 29 | if bufSize <= 0 { 30 | bufSize = 4096 31 | } 32 | mu := new(sync.Mutex) 33 | a := newHalfConn(mu, addrA, bufSize) 34 | b := newHalfConn(mu, addrB, bufSize) 35 | return &Conn{mu: mu, r: a, w: b}, &Conn{mu: mu, r: b, w: a} 36 | } 37 | 38 | // Read reads data from the connection. It can be made to time out and return a 39 | // net.Error with Timeout() == true after a deadline or a per-Read timeout; see 40 | // SetDeadline, SetReadDeadline, and SetTimeout. 41 | func (c *Conn) Read(b []byte) (n int, err error) { 42 | var t timer 43 | c.mu.Lock() 44 | defer c.mu.Unlock() 45 | t.Set(c.rd, c.t) 46 | if n, err = c.r.read(b, &t, c.r.addr); err == io.EOF { 47 | c.close() 48 | } 49 | return 50 | } 51 | 52 | // Write writes data to the connection. It can be made to time out and return a 53 | // net.Error with Timeout() == true after a deadline or a per-Write timeout; see 54 | // SetDeadline, SetWriteDeadline, and SetTimeout. 55 | func (c *Conn) Write(b []byte) (n int, err error) { 56 | var t timer 57 | c.mu.Lock() 58 | defer c.mu.Unlock() 59 | t.Set(c.wd, c.t) 60 | return c.w.write(b, &t, c.r.addr) 61 | } 62 | 63 | // Close closes the connection. Any blocked Read or Write operations will be 64 | // unblocked and return errors. 65 | func (c *Conn) Close() error { 66 | c.mu.Lock() 67 | defer c.mu.Unlock() 68 | c.close() 69 | return nil 70 | } 71 | 72 | // LocalAddr returns the local network address. 73 | func (c *Conn) LocalAddr() net.Addr { 74 | return netAddr(c.r.addr) 75 | } 76 | 77 | // RemoteAddr returns the remote network address. 78 | func (c *Conn) RemoteAddr() net.Addr { 79 | return netAddr(c.w.addr) 80 | } 81 | 82 | // SetDeadline sets the Read and Write deadlines associated with the connection. 83 | // It is equivalent to calling both SetReadDeadline and SetWriteDeadline. 84 | func (c *Conn) SetDeadline(t time.Time) error { 85 | c.mu.Lock() 86 | c.rd = t 87 | c.wd = t 88 | c.mu.Unlock() 89 | return nil 90 | } 91 | 92 | // SetReadDeadline sets the deadline for future Read calls. A zero value for t 93 | // means Read will not time out (but see SetTimeout). 94 | func (c *Conn) SetReadDeadline(t time.Time) error { 95 | c.mu.Lock() 96 | c.rd = t 97 | c.mu.Unlock() 98 | return nil 99 | } 100 | 101 | // SetWriteDeadline sets the deadline for future Write calls. Even if Write 102 | // times out, it may return n > 0, indicating that some of the data was 103 | // successfully written. A zero value for t means Write will not time out (but 104 | // see SetTimeout). 105 | func (c *Conn) SetWriteDeadline(t time.Time) error { 106 | c.mu.Lock() 107 | c.wd = t 108 | c.mu.Unlock() 109 | return nil 110 | } 111 | 112 | // SetTimeout sets the per-call timeout for future Read and Write calls. It 113 | // works in addition to any configured deadlines. A value <= 0 for d means 114 | // Read and Write will not time out (unless a deadline is set). 115 | func (c *Conn) SetTimeout(d time.Duration) error { 116 | if d < 0 { 117 | d = 0 118 | } 119 | c.mu.Lock() 120 | c.t = d 121 | c.mu.Unlock() 122 | return nil 123 | } 124 | 125 | // close closes the connection. 126 | func (c *Conn) close() { 127 | if c.r.buf != nil { 128 | c.r.buf = nil 129 | c.r.eof = true 130 | c.r.Broadcast() 131 | 132 | c.w.eof = true 133 | c.w.Broadcast() 134 | 135 | c.rd = time.Time{} 136 | c.wd = time.Time{} 137 | c.t = 0 138 | } 139 | } 140 | 141 | // halfConn implements a unidirectional data pipe. 142 | type halfConn struct { 143 | sync.Cond 144 | 145 | addr string // Reader's address 146 | buf []byte // Read/write buffer 147 | off int // Read offset in buf 148 | eof bool // Writer closed flag 149 | } 150 | 151 | // newHalfConn creates a new halfConn instance. 152 | func newHalfConn(mu *sync.Mutex, addr string, bufSize int) *halfConn { 153 | return &halfConn{ 154 | Cond: *sync.NewCond(mu), 155 | addr: addr, 156 | buf: make([]byte, 0, bufSize), 157 | } 158 | } 159 | 160 | // read copies data from the buffer into b. 161 | func (c *halfConn) read(b []byte, t *timer, addr string) (n int, err error) { 162 | for { 163 | switch { 164 | case c.buf == nil: 165 | return n, io.EOF 166 | case t.Expired(): 167 | return n, netTimeout("mock.Conn(" + addr + "): read timeout") 168 | case len(b) == 0: 169 | return 170 | case len(c.buf) > 0: 171 | n = copy(b, c.buf[c.off:]) 172 | if c.off += n; c.off == len(c.buf) { 173 | c.buf = c.buf[:0] 174 | c.off = 0 175 | if c.eof { 176 | return n, io.EOF 177 | } 178 | } 179 | c.Broadcast() 180 | return 181 | } 182 | if t.Schedule(&c.Cond) { 183 | defer t.Stop() 184 | } 185 | c.Wait() 186 | } 187 | } 188 | 189 | // write copies data from b into the buffer. 190 | func (c *halfConn) write(b []byte, t *timer, addr string) (n int, err error) { 191 | for { 192 | switch { 193 | case c.eof: 194 | return n, io.EOF 195 | case t.Expired(): 196 | return n, netTimeout("mock.Conn(" + addr + "): write timeout") 197 | case len(b) == 0: 198 | return 199 | case len(c.buf) == 0: 200 | c.buf = c.buf[:copy(c.buf[:cap(c.buf)], b)] 201 | c.Broadcast() 202 | if n += len(c.buf); len(b) == len(c.buf) { 203 | return 204 | } 205 | b = b[len(c.buf):] 206 | default: 207 | if free := cap(c.buf) - len(c.buf); free < len(b) { 208 | if free += c.off; free < len(b) { 209 | break // Will block anyway, let the reader(s) catch up 210 | } 211 | c.buf = c.buf[:copy(c.buf, c.buf[c.off:])] 212 | c.off = 0 213 | } 214 | n += copy(c.buf[len(c.buf):cap(c.buf)], b) 215 | c.buf = c.buf[:len(c.buf)+len(b)] 216 | return 217 | } 218 | if t.Schedule(&c.Cond) { 219 | defer t.Stop() 220 | } 221 | c.Wait() 222 | } 223 | } 224 | 225 | // timer interrupts blocked Read/Write calls at a specific deadline or after a 226 | // per-call timeout, whichever is earlier. 227 | type timer struct { 228 | *time.Timer 229 | now, rem int64 230 | } 231 | 232 | // Set configures timer expiration parameters. 233 | func (t *timer) Set(deadline time.Time, timeout time.Duration) { 234 | if dnz := !deadline.IsZero(); dnz || timeout > 0 { 235 | t.now = time.Now().UnixNano() 236 | t.rem = int64(timeout) 237 | if dnz { 238 | dt := deadline.UnixNano() - t.now 239 | if timeout <= 0 || dt < int64(timeout) { 240 | t.rem = dt 241 | } 242 | } 243 | } 244 | } 245 | 246 | // Expired returns true if the timer has expired. 247 | func (t *timer) Expired() bool { 248 | if t.now != 0 { 249 | if t.Timer != nil { 250 | now := time.Now().UnixNano() 251 | t.rem -= now - t.now 252 | t.now = now 253 | } 254 | return t.rem <= 0 255 | } 256 | return false 257 | } 258 | 259 | // Schedule configures the timer to call c.Broadcast() when the timer expires. 260 | // It returns true if the caller should defer a call to t.Stop(). 261 | func (t *timer) Schedule(c *sync.Cond) bool { 262 | if t.now != 0 { 263 | if t.Timer == nil { 264 | t.Timer = time.AfterFunc(time.Duration(t.rem), func() { 265 | // c.L must be held to guarantee that the caller is waiting 266 | c.L.Lock() 267 | defer c.L.Unlock() 268 | c.Broadcast() 269 | }) 270 | return true 271 | } 272 | t.Reset(time.Duration(t.rem)) 273 | } 274 | return false 275 | } 276 | 277 | // netAddr implements net.Addr for the "mock" network. 278 | type netAddr string 279 | 280 | func (netAddr) Network() string { return "mock" } 281 | func (a netAddr) String() string { return string(a) } 282 | 283 | // netTimeout implements net.Error with Timeout() == true. 284 | type netTimeout string 285 | 286 | func (t netTimeout) Error() string { return string(t) } 287 | func (netTimeout) Timeout() bool { return true } 288 | func (netTimeout) Temporary() bool { return true } 289 | -------------------------------------------------------------------------------- /mock/tls.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go-IMAP Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mock 6 | 7 | import ( 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/pem" 14 | "math/big" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | var tlsCfg = struct { 20 | sync.Mutex 21 | c, s *tls.Config 22 | }{} 23 | 24 | func clientTLS() *tls.Config { 25 | tlsCfg.Lock() 26 | defer tlsCfg.Unlock() 27 | if tlsCfg.c == nil { 28 | tlsCfg.c, tlsCfg.s = newConfig() 29 | } 30 | return tlsCfg.c 31 | } 32 | 33 | func serverTLS() *tls.Config { 34 | tlsCfg.Lock() 35 | defer tlsCfg.Unlock() 36 | if tlsCfg.s == nil { 37 | tlsCfg.c, tlsCfg.s = newConfig() 38 | } 39 | return tlsCfg.s 40 | } 41 | 42 | func newConfig() (client, server *tls.Config) { 43 | now := time.Now() 44 | tpl := x509.Certificate{ 45 | SerialNumber: new(big.Int).SetInt64(42), 46 | Subject: pkix.Name{CommonName: ServerName}, 47 | NotBefore: now.Add(-2 * time.Hour).UTC(), 48 | NotAfter: now.Add(2 * time.Hour).UTC(), 49 | BasicConstraintsValid: true, 50 | IsCA: true, 51 | } 52 | priv, err := rsa.GenerateKey(rand.Reader, 512) 53 | if err != nil { 54 | panic(err) 55 | } 56 | crt, err := x509.CreateCertificate(rand.Reader, &tpl, &tpl, &priv.PublicKey, priv) 57 | if err != nil { 58 | panic(err) 59 | } 60 | key := x509.MarshalPKCS1PrivateKey(priv) 61 | pair, err := tls.X509KeyPair( 62 | pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: crt}), 63 | pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: key}), 64 | ) 65 | if err != nil { 66 | panic(err) 67 | } 68 | root, err := x509.ParseCertificate(crt) 69 | if err != nil { 70 | panic(err) 71 | } 72 | server = &tls.Config{Certificates: []tls.Certificate{pair}} 73 | client = &tls.Config{RootCAs: x509.NewCertPool(), ServerName: ServerName} 74 | client.RootCAs.AddCert(root) 75 | return 76 | } 77 | --------------------------------------------------------------------------------